Today I came across a tweet:
https://twitter.com/BenLesh/status/1717272722193965511
I was shocked!
How is this possible? I have always thought that throw
is synchronous!
In case you don't know why this is weird:
synchronous code should run synchronously even in async function because the process of creating promise itself is synchronous (If everything is promise, what is the point of promise?)
proof:
console.log(123);
(async()=>{
try{
return await (async()=>{console.log(456)})()
}catch(e){
console.log({e},"error!")
}
})();
console.log(789)
but this obviously not the case with throw
!
console.log(123);
(async()=>{
try{
return await (async()=>{throw 456})()
}catch(e){
console.log({e},"error!")
}
})();
console.log(789)
async function
convert the suppose synchronous throw
into asynchronous throw
, what?!!
The closest explanation I can come up with is throw
is return
in disguise because return
behave in the similar way: async function change return
into returning promise. But still this doesn't explain everything and how can we make sense out of it.
update:
answer in these comments (read all replies)
TLDR, throw happens synchronously but is reported asynchronously
Top comments (13)
I am a bit puzzled why would someone expect that catch to be hit. Rejected promise passed on without await, so it is not handled in the
test
function of the original tweet.It works just the way it is written.
people expect it to be hit, because people expect throw to be regular synchronous statement just like console.log or 1+1, etc etc
Well, it only means people lack understanding of async/await, essentially the code is equivalent to
and no one would expect catch to be hit in that function, since it is the constructor wrapped in try/catch and there's no reason for it to error out.
Or maybe it is me being foolish, I dunno.
this is not because people dont understand async await, it is because people(including me) dont understand throw works (we thought we understand throw works)
to understand this, let exam how synchronous code works
very straight forward right?
after 1 it is 2, after 2 it is 3
next we exam how throw works
after 1 it is throw, 3 will not be printed because throw happen after 1 and before 3
at this point we pretty much learned that throw is synchronous just like other synchronous code work
Next let us exam how synchronous code work in aysnc function
this is an expected result, synchronous code in async function is still synchronous
now based on our understanding of previous examples, we would assume that throw will also run synchronously and we will get the same result, which mean 3 will not be printed
however the result prove that synchronous throw in async function is NOT synchronous because 3 is printed and throw happen after 3
async function convert synchronous throw into asynchronous throw
which mean it is impossible to throw synchronous error in async function and this is weird because throwing synchronous error should be something that is always possible (just like how we throw normally)
we just dont understand throw enough
Oh, I see what you mean. But had the throw been synch here, it would cause error bursting out of the Promise object, bypassing the reject.
that is what we want
if we can write both synchronous console.log and asynchronous console.log in async function, then we should also able to write both synchronous throw and asynchronous throw in async function
but now we don't have the choice, we don't have that control
there is a incompleteness in JS async function/promise
The point is not that
throw
is Async, it's that it's in aPromise
rejection. Thethrow
happened synchronously and thePromise
returned by the function was set to rejected immediately. The effect you are seeing is when the rejection is printed to the screen. This happens on the next main loop and this is because you returned aPromise
. ThePromise
constructor is synchronous (your log and your throw are immediately executed), however, the response to thePromise
will never - ever - be on the same main loop cycle - so yourthrow
is reported later.When you use an
async
function you are passing the handling of any throw within it to thePromise
that is created for you. Unless you have the result of thePromise
you cannot get the error. In the Twitter example, thePromise
result is not available in the handler so thecatch
never happens.It is a documented feature of promises - just one to catch you out. A simple rule of thumb, if you have a try-catch you must
await
thePromise
because otherwise, you don't have its result as it will not ever be returned immediately even if there is nothing deeper being awaited, so there will be nothing tocatch
.The rule: no result of a
Promise
will ever be processed synchronously, it's at least on the next cycle of the main loop.you are right about throw run synchronously and is reported asynchronously
throw becomes rejected(I always see throw and reject as 2 different things that do similar thing) is something that I totally not expecting, this mean that we will never able to report the throw synchronously(even if throw happens synchronously)
"if it happens synchronously, it should be reported synchronously"
this is what behavior that we should expect
I disagree. Promises always return async. That's a hard and fast rule. You are using the result of a promise, it will never happen synchronously.
my thought is simpler, if synchronous error is thrown, there would no resolution or rejection, aka no promise
throw happen in promise executor should behave the same with throw happen before promise constructor is called
just like console.log happen in promise executor should behave the same with console.log happen before promise constructor is called
I believe reject should handle failing of promise and should not failing of creation of promise
But the Promise was made as soon as you called an async function. The throw is inside the promise. Write it out as new Promise(resolve, reject) and then statements and you'll see the Promise already exists. The Promise constructor was called. The only way to throw before it is to throw the error before the funciton call.
Which is I think is weird, take a look at below example
as you can see, there is simply no Car, the Car object is not created
This is what I want to expect: If I throw an error inside the promise constructor, it should simply result in no Promise being created, rather than delegating the error to the promise internally.
Rejection should handle the failure of the promise, not the failure of promise construction. This also seems very strange when compared to a real-life scenario.
Let's consider a situation where Company A and Company B want to collaborate to develop a piece of land. To do this, they need to draft a contract and sign an agreement. The agreement outlines what they will build once they acquire the land and also protects them from partner withdrawal.
If we examine this closely, it's quite similar to how promises work:
What's happening now:
Drafting and signing the contract → Equivalent to the promise executor.
What will happen in the future:
Buying the land and deciding what to build → Equivalent to a resolve.
Partner withdrawal → Equivalent to a reject.
Now, let's consider a situation where, for some unexpected reason, Company A decides not to sign the contract(throws an error). Delegating the error(now) to the promise rejection(future) is like making the "not signing the contract" decision equivalent to a breach of the contract.
Do you see how absurd this is? Company A did not sign the contract but is being held accountable by the contract!
What really should happen is that A and B should simply cancel the contract(no promise is created) and move on.
no contract, no buying land, no partner withdrawal
no promise, no resolution, no rejection
everybody simply continue doing what they want to do next
Errors that occur in a Promise are deferred until the task completes.
To be specific, await, then, and catch are all deferred until the task completes.
This is an interesting topic.