DEV Community

Acid Coder
Acid Coder

Posted on • Updated on

JS being weird again: forcibly convert synchronous statement to asynchronous with async function

Today I came across a tweet:

Image description

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)
Enter fullscreen mode Exit fullscreen mode

Image description

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)
Enter fullscreen mode Exit fullscreen mode

Image description

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)

Collapse
 
bwca profile image
Volodymyr Yepishev

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.

async function willReject() {
    throw new Error ('Oh, no, a twitter drama!');
}

async function test() {
    try {
        return willReject();
    } catch(reason) {
        console.log('missed');
    }
}

test().catch(console.log);
Enter fullscreen mode Exit fullscreen mode
Collapse
 
tylim88 profile image
Acid Coder • Edited

people expect it to be hit, because people expect throw to be regular synchronous statement just like console.log or 1+1, etc etc

Collapse
 
bwca profile image
Volodymyr Yepishev • Edited

Well, it only means people lack understanding of async/await, essentially the code is equivalent to

function test() {
    try {
        return new Promise((res, rej) => {
            rej('Oh, no, a twitter drama!')
        })
    } catch(reason) {
        console.log('never gets hit as expected');
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Thread Thread
 
tylim88 profile image
Acid Coder • Edited

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

Image description
very straight forward right?

after 1 it is 2, after 2 it is 3

next we exam how throw works

Image description
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

Image description

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

Image description

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

Thread Thread
 
bwca profile image
Volodymyr Yepishev

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.

Thread Thread
 
tylim88 profile image
Acid Coder • Edited

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

Thread Thread
 
miketalbot profile image
Mike Talbot ⭐ • Edited

The point is not that throw is Async, it's that it's in a Promise rejection. The throw happened synchronously and the Promise 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 a Promise. The Promise constructor is synchronous (your log and your throw are immediately executed), however, the response to the Promise will never - ever - be on the same main loop cycle - so your throw is reported later.

When you use an async function you are passing the handling of any throw within it to the Promise that is created for you. Unless you have the result of the Promise you cannot get the error. In the Twitter example, the Promise result is not available in the handler so the catch 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 the Promise 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 to catch.

The rule: no result of a Promise will ever be processed synchronously, it's at least on the next cycle of the main loop.

Thread Thread
 
tylim88 profile image
Acid Coder • Edited

you are right about throw run synchronously and is reported asynchronously
Image description

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

Thread Thread
 
miketalbot profile image
Mike Talbot ⭐

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.

Thread Thread
 
tylim88 profile image
Acid Coder • Edited

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

Thread Thread
 
miketalbot profile image
Mike Talbot ⭐

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.

Thread Thread
 
tylim88 profile image
Acid Coder • Edited

Which is I think is weird, take a look at below example

class Car {
  constructor(executor) {
    executor()
  }
}
console.log(1)
const myCar1 = new Car(()=>{
    throw 2
})
console.log(3)
Enter fullscreen mode Exit fullscreen mode

Image description

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

Collapse
 
artxe2 profile image
Yeom suyun

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.