v8/v8.dev

Is there an error on the post named "Faster async functions and promises"?

Closed this issue · 4 comments

target post: https://v8.dev/blog/fast-async

Summary

Hello! I'm a student in Korea(South) Studying JavaScript, and I created this issue because of the error on the post named "Faster async functions and promises".
I think the code example's "the correct behavior" below is incorrect.

const p = Promise.resolve();

(async () => {
  await p;
  console.log('after: await');
})();

p.then(() => console.log('tick: a'))
  .then(() => console.log('tick: b'));

Details

When I first encountered this code snippet on the post, I expected the result of it is like below.

after: await
tick: a
tick: b

But the post and the presentation of post both said that the "correct result" of running the example is below.

tick: a
tick: b
after: await

I shocked by this, and rush to the terminal, run this example on different node versions(8, 10, 11, 12, 18, 20).
The results are below.

  • "after: await" first -> version 8
  • "tick: a" first -> version 10
  • "tick: a" first -> version 11
  • "after: await" first -> version 12 and later

Even the result of Chrome browser console is also "after: await" first.
So I dived in to the mechanism of the code example.

Deep Dive

First the const p has resolved Promise by the code below.

const p = Promise.resolve();

And IIFE below calls the async function, which has the code await p; and console.log('after: await');

(async () => {
  await p;
  console.log('after: await');
})();

By await keyword, even the p is resolved Promise, suspend execution of the async function and rest of the task is pushed to the microtask queue. (You can find this feature of await keyword on MDN.

Since async function is suspended, the code below will executed.

p.then(() => console.log('tick: a'))
 .then(() => console.log('tick: b'));

Because p is resolved Promise, both callback () => console.log('tick: a') and () => console.log('tick: a') are going to pushed to the microtask queue.

So we can say that the tasks in the microtask queue are pushed by the below order.

  1. console.log('after: await')
  2. () => console.log('tick: a')
  3. () => console.log('tick: b')

There's no rest of code below, so on the next tick, suspended async function will be resumed.
But the scheduled Microtask, console.log('after: await') can be executed before async function resumed. (related explaination can be found on MDN. find 'queueMicrotask' keyword on this document!)

So after microtask console.log('after: await') executed, async function and IIFE will ends.
Rest of microtasks will be brought to call stack on the next tick by the event loop, and they will also executed by the order.

Finally we can check the console, and find that the results are same below.

after: await
tick: a
tick: b

So I Hope...

I would like someone to provide feedback on whether my code analysis was accurate or not.

Apart from this issue, I would like to appreciate to the two authors @MayaLekova, @bmeurer for writing such a great article.

Hi @sscoderati, your deep dive is entirely correct with respect to the current JavaScript spec. In fact, this is what the post is about: the intuitive behaviour, as you describe, was not correct according to an older version of the JavaScript spec (because it required additional wrapping in additional promises), but we had a bug (up to Node version 8) which did it anyway. We fixed the bug in Node 10 to be "correct" as per spec, but in parallel we also proposed a change to the JavaScript specification which made the previous "incorrect" behaviour actually be correct. This change was accepted, and as of Node 12 we went back to the old behaviour, except now it was correct instead of being a bug.

So, the short answer is that the "correct" behaviour, as written in the blog post, was correct back then, but we changed JavaScript so that it's incorrect now and the more obvious (and efficient!) behaviour is now correct.

Maybe we should update the blog post with a note that the behaviour has since changed...

Hi @sscoderati, your deep dive is entirely correct with respect to the current JavaScript spec. In fact, this is what the post is about: the intuitive behaviour, as you describe, was not correct according to an older version of the JavaScript spec (because it required additional wrapping in additional promises), but we had a bug (up to Node version 8) which did it anyway. We fixed the bug in Node 10 to be "correct" as per spec, but in parallel we also proposed a change to the JavaScript specification which made the previous "incorrect" behaviour actually be correct. This change was accepted, and as of Node 12 we went back to the old behaviour, except now it was correct instead of being a bug.

So, the short answer is that the "correct" behaviour, as written in the blog post, was correct back then, but we changed JavaScript so that it's incorrect now and the more obvious (and efficient!) behaviour is now correct.

Maybe we should update the blog post with a note that the behaviour has since changed...

I'm grateful for your response @LeszekSwirski! I hadn't considered that the change in JavaScript's specification could be the reason, but thanks to you, I'm very pleased to understand it now :)
I came across this blog post while searching Google for information on async/await syntax. As a student studying JavaScript like myself, I think others in a similar position might encounter similar confusion when reading this section. If it's okay, could I open a pull request to contribute to the brief notes you mentioned?

Sure, please feel free, contributions are always welcome.

This issue is completed with #737