DEV Community 👩‍💻👨‍💻

Erik Smith
Erik Smith

Posted on

Async Await Behavior

Demonstrate async/await function behavior in comparison with standard synchronous behavior.

https://replit.com/@365Erik/Async-Await-Behavior

A Promise to Share

We often find promises implemented in situ like p.then().catch().finally() but a variable pointing to a promise can be referenced in multiple locations of your code base. Here we create a single promise for use in two functions: one async and one standard.

const sharedPromise = new Promise((resolve) => {
  setTimeout(() => resolve("sharedPromise has resolved"), 1000);
});
Enter fullscreen mode Exit fullscreen mode

Async Implementation

const asyncFunc = async () => {
  console.log(`asyncFunc sees ${await sharedPromise}`);
  console.log("asyncFunc's second statement fires only after sharedPromise has resolved");
}

asyncFunc();
console.log("asyncFunc is moved into the queue and the program executes the next statement");
Enter fullscreen mode Exit fullscreen mode
asyncFunc is moved into the queue and the program executes the next statement
asyncFunc sees sharedPromise has resolved
asyncFunc's second statement fires only after sharedPromise has resolved
Enter fullscreen mode Exit fullscreen mode

In the above implementation, await sharedPromise introduces blocking behavior within the function execution context. This means the stack will not proceed to the next line within the function until the awaited promise resolves. The entire function execution stack is put in a queue until the promise resolves to unblock it. Meanwhile, the rest of the application continues moving forward, and prints the message asyncFunc is moved into the queue... while asyncFunc awaits the resolution of our sharedPromise.

Standard Function

const syncFunc = () => {
  sharedPromise.then(result => console.log(`syncFunc sees ${result}`));
  console.log("syncFunc's second statement fires immediately without waiting for sharedPromise to resolve");
}

syncFunc();
console.log("syncFunc exits immediately and the program moves onto the next statement");
Enter fullscreen mode Exit fullscreen mode
syncFunc's second statement fires immediately without waiting for sharedPromise to resolve
syncFunc exits immediately and the program moves onto the next statement
syncFunc sees sharedPromise has resolved
Enter fullscreen mode Exit fullscreen mode

Above, we're using a regular function and the p.then(result => console.log(result)) pattern to log when sharedPromise resolves. There is no blocking behavior within the function context, so we proceed through the statements, exit the function, and onto the final console.log statement. We'll get a message that syncFunc sees sharedPromise has resolved about one second later.

Altogether, Now

const sharedPromise = new Promise((resolve) => setTimeout(() => resolve("sharedPromise has resolved"), 1000));

const asyncFunc = async () => {
  console.log(`asyncFunc sees ${await sharedPromise}`);
  console.log("asyncFunc's second statement fires only after sharedPromise has resolved");
}

const syncFunc = () => {
  sharedPromise.then(result => console.log(`syncFunc sees ${result}`));
  console.log("syncFunc's second statement fires immediately without waiting for sharedPromise to resolve");
}

asyncFunc();
console.log("first statement after asyncFunc");
syncFunc();
console.log("first statement after syncFunc");
Enter fullscreen mode Exit fullscreen mode
first statement after asyncFunc
syncFunc's second statement fires immediately without waiting for sharedPromise to resolve
first statement after syncFunc
asyncFunc sees sharedPromise has resolved
asyncFunc's second statement fires only after sharedPromise has resolved
syncFunc sees sharedPromise has resolved
Enter fullscreen mode Exit fullscreen mode

Below is a rough representation of what is happening in our callstack to explain the seemingly shuffled up results, which despite appearances are in correct and linear order.

call asyncFunc
|-- console.log must await sharedPromised resolution
|-- move asyncFunc into the queue
|-- check queue

console.log **first statement after asyncFunc**

check queue

call syncFunc
|-- check queue
|-- set up a promise chain with `sharedPromise.then()` and put in queue
|- check queue
|- console.log **syncFunc's second statement fires immediately...**

check queue

console.log **first statement after syncFunc**

check queue repeatedly

check queue: sharedPromise has resolved!

put asyncFunc back on the callstack
|_ console.log **asyncFunc sees sharedPromise has resolved**
|_ console.log **asyncFunc's second statement fires only after...**

put syncFunc->sharedPromise.then statement back on stack
|_ console.log **syncFunc sees sharedPromise has resolved**

Enter fullscreen mode Exit fullscreen mode

Oldest comments (3)

Collapse
 
peerreynders profile image
peerreynders • Edited on
"asyncFunc is moved into the queue and the program executes the next statement"
Enter fullscreen mode Exit fullscreen mode

That may create the wrong impression with some readers. asyncFunc actually begins to run synchronously until it hits the statement with await. async function is more of a state machine that starts executing synchronously, running until it encounters an awaited promise.

const asyncFunc = async () => {
  console.log('before await');
  console.log(`asyncFunc sees ${await sharedPromise}`)
  console.log('asyncFunc's second statement fires only after sharedPromise has resolved')
}
Enter fullscreen mode Exit fullscreen mode

would result in

before await
first statement after asyncFunc
syncFunc's second statement ...
...
Enter fullscreen mode Exit fullscreen mode

(hinted at in your later linear representation)

Also it's setTimeout that creates a task on the task queue. sharedPromise only is resolved when that scheduled task executes. So the remainder of the asyncFunc function only goes on the microtask queue once sharedPromise has resolved ~1000ms later.


const somePromise = Promise.resolve(42);
const resolve = (value) => console.log(`Resolved to: ${value}`);
const task = () => {
  console.log('before then');
  somePromise.then(resolve);
  console.log('after then');
};
setTimeout(task, 1000);
console.log('task scheduled');
Enter fullscreen mode Exit fullscreen mode
task scheduled
before then
after then
Resolved to: 42
Enter fullscreen mode Exit fullscreen mode

i.e. when chaining, the chained code will always run after the synchronous code - regardless of whether the promise has already settled or not (there is no "checking the queue").

Because the microtask queue always empties before any new events are processed or tasks are run, somePromise has resolved by the time we hit "before then" - so () => resolve(42) can immediately go onto the microtask queue - and resolve will only run once the current call stack empties out.

Collapse
 
365erik profile image
Erik Smith Author

I take your point on clarity and will have to ponder how to reword some of it without bogging it down with too much detail.

Collapse
 
peerreynders profile image
peerreynders

I believe the issue is attempting to discuss async/await in isolation of the foundational concepts.

Tasks, microtasks, queues and schedules

  • Foundation: event loop, task, microtask (queues)
  • How promises relate to mictrotasks
  • How async/await relates to promises

In the presence of working knowledge about the event loop, tasks, microtasks, and promises one can call on the pertinent details without getting bogged down in supplementary explanations.

Forming an effective mental model around async/await without understanding the rest is prone to oversimplification that will eventually cause problems (like failing to use functions like Promise.allSettled() and friends).

await vs return vs return await

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.