-10

I found that it could be tricky without thinking careful. When exactly does an async function return to the caller?

I will intentionally not making this into a snippet so that the readers can guess what the output is:

What would be printed out:

async function foo() {
  console.log("COMING INTO foo");

  await new Promise((resolve) => {
    setTimeout(resolve, 3000);
  });
  console.log("Right before return");
  return 123;
}

function takeIt(fn) {
  console.log("STARTING");
  foo();
  console.log("Function returned");
}

takeIt(foo);

and why?

Perspective one can be: Well, we all know async function will pause for the setTimeout and sleep for 3 seconds, and it won't return later on... so

STARTING
COMING INTO foo
Right before return
Function returned

Perspective two can be: Well, foo() returns 123 (or not return anything and that would mean to return undefined), and it is a promise... and returning a promise is instantaneous, so it is:

STARTING
Function returned
COMING INTO foo
Right before return

Now if we add one more tweak to the plot:

async function foo() {
  await Promise.resolve(678);

  console.log("COMING INTO foo");

  await new Promise((resolve) => {
    setTimeout(resolve, 3000);
  });
  console.log("Right before return");
  return 123;
}

function takeIt(fn) {
  console.log("STARTING");
  foo();
  console.log("Function returned");
}

takeIt(foo);

and there can be some more perspectives... I guessed what is printed for all 3 cases, before I ran the program, and got the correct answers. But I did not know precisely how it works, but I made a guess. Can somebody shred light on precisely how it works, and if needed, I will post my answer a few days later as to how I think it exactly worked.

The question is: when does foo() return and precisely, how does it work? What is the guiding principle?

nonopolarity
  • 146,324
  • 131
  • 460
  • 740

2 Answers2

3

An async function will yield control flow back to its caller (that is, log Function returned in your example) once the async function's code runs into an await. It will not yield flow back before then. So here:

async function foo() {
  console.log("COMING INTO foo");

  await new Promise((resolve) => {
    setTimeout(resolve, 3000);
  });

Calling foo will immediately log COMING INTO foo, and then due to the await, control flow yields back to the caller of foo.

Similarly:

async function foo() {
  await Promise.resolve(678);

  console.log("COMING INTO foo");

The await comes first, so here, control flow is yielded back before foo logs anything.

Here, "control flow is yielded back" is synonymous with "returns a Promise to the caller."

CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • You said "An async function will yield control flow back to its caller once the async function's code runs into an await." Do we just take this as "this is the way it is and it is like the bible"... or is there a way to understand how or why it works this way? – nonopolarity Dec 08 '21 at 05:37
  • JavaScript is single-threaded and manages to perform a lot despite this. The reason it can be efficient is partly due to how it deals with this kind of situations, not using more time than needed but instead queuing events for later so that it can continue doing something else. – fast-reflexes Dec 08 '21 at 05:45
  • @nonopolarity It's pretty clear just from running the code. JavaScript is single-threaded, so as soon as an `await` is encountered in an async function, there's no more work for the async function to do at that time. – CertainPerformance Dec 08 '21 at 05:46
  • @CertainPerformance could you have thought of it as: the async function is a giant promise, and it is first returned to the caller, and then the content inside the promise is executed. It sounds possible too. – nonopolarity Dec 08 '21 at 05:49
  • 1
    Kind of. Just like with the Promise constructor, the code directly inside it runs immediately when it's called, until an `await` is encountered. If you're curious about the specification details of how async functions get suspended, see here: https://tc39.es/ecma262/#sec-asyncblockstart – CertainPerformance Dec 08 '21 at 05:50
  • the promise constructor... you mean the executor `execFn` passed as `new Promise(execFn)`, or do you really mean the constructor of the class `Promise`? – nonopolarity Dec 08 '21 at 05:57
  • @nonopolarity The argument passed into the Promise constructor. Like https://stackoverflow.com/a/49911346 – CertainPerformance Dec 08 '21 at 05:59
  • I think you mean the executor, then. It may be better to use common terminology that everybody agreed on – nonopolarity Dec 08 '21 at 06:06
  • 2
    @nonopolarity I think the term "Promise constructor" is [quite widely](https://www.google.com/search?q=%22promise+constructor%22) known to be the construct that takes the form `new Promise((resolve, reject) => { /* more code here */`. Never heard it called the executor before. – CertainPerformance Dec 08 '21 at 06:10
  • I see... you are talking about constructor as in the age of prototypal inheritance... note that now we have `class`, there really is a `constructor`. You said "Kind of. Just like with the Promise constructor, the code directly inside it runs immediately when it's called, until an await is encountered"... you mean if it is an async function? Besides, it is not "the code directly inside it runs immediately when it's called". It is "the code directly inside it runs immediately" (no matter what). – nonopolarity Dec 08 '21 at 06:38
  • "The code directly inside it", is called an executor: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise#syntax – nonopolarity Dec 08 '21 at 06:40
  • @nonopolarity prototypal inheritance still apply, class in JS are simply sintatic sugar on top of previous definition, so "wrappers" for let use class structure and terminology – Carmine Tambascia Feb 13 '22 at 20:18
-1

My answer is, at times it is difficult to tell exactly how an async function behaves, and here is the conversion:

async function foo() {
  console.log("COMING INTO foo");

  await new Promise((resolve) => {
    setTimeout(resolve, 3000);
  });
  console.log("Right before return");
  return 123;
}

function takeIt(fn) {
  console.log("STARTING");
  foo();
  console.log("Function returned");
}

takeIt(foo);

For the code above, it can be viewed exactly the same as:

function foo() {
  // any code before the first await goes here, which is executed immediately:
  console.log("COMING INTO foo");

  // when the first await is encountered, the whole thing becomes a promise
  // and is returned to the caller:
  return new Promise((resolve0, reject0) => {
    const promise0 = new Promise((resolve) => {
      setTimeout(resolve, 3000);
    });

    promise0.then(v0 => { 
      // the more await there are in the original code, the more "nested"
      // `then` there will be. It does indeed become a nesting hell
      console.log("Right before return");

      // no matter how deep the nesting is, the last returned value
      // is called with resolve0
      resolve0(123);
    });
  });
}

So the idea is: some code will be executed immediately, synchronously, but at the first point of await, it all of a sudden will become a gigantic promise, and its final resolved value would be the last line of the async function. So this "gigantic promise" is like floating in the air, waiting to be invoked and invoked again (we can think of it as handlers and handlers being invoked again and again, and are deeply nested).

In the async function, all the variables that get assigned the value from await are as if they are in the same scope, but in the converted code, the then handler can access those variables by using the outer scope, and so when the then handler is called, it is making use of closure to access these outer scope variables.

The purpose of this question and answer is so that we understand exactly what is happening, because I found some programmers, including myself previously before understanding this, had a vague idea of what is happening but cannot really tell, and writing code without really understanding it may not be a good practice.

nonopolarity
  • 146,324
  • 131
  • 460
  • 740