JavaScript
Async/Await
Concurrency
Asynchronous Programming
Execution Order

Async and Await - How is order of execution maintained?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

async and await make asynchronous JavaScript read like synchronous code, but they do not remove the event loop. Order is maintained because JavaScript runs synchronous statements immediately, suspends an async function at each await, and later resumes that function through the promise microtask queue.

That means there are really two kinds of ordering to understand: the lexical order inside one async function, and the scheduling order between synchronous code, promise continuations, and timer callbacks. Most confusion comes from mixing those two models together.

What await Actually Does

When JavaScript reaches await somePromise, it does three things:

  • Evaluates the promise expression right away.
  • Suspends only the current async function.
  • Schedules the remainder of that function to resume later, after the promise settles.

It does not block the thread, and it does not pause unrelated code.

javascript
1function delay(ms, value) {
2  return new Promise((resolve) => {
3    setTimeout(() => resolve(value), ms);
4  });
5}
6
7async function demo() {
8  console.log("A");
9  const result = await delay(100, "done");
10  console.log("B", result);
11}
12
13console.log("0");
14demo();
15console.log("1");

The output is:

text
10
2A
31
4B done

demo() starts immediately, prints A, pauses at await, and then resumes later. That later resumption is why 1 appears before B done.

Event Loop, Microtasks, and Timers

Promise continuations run as microtasks. Timer callbacks such as setTimeout run as macrotasks. After the current synchronous call stack finishes, JavaScript drains the microtask queue before taking the next macrotask.

javascript
1console.log("sync start");
2
3Promise.resolve().then(() => {
4  console.log("microtask");
5});
6
7setTimeout(() => {
8  console.log("timer");
9}, 0);
10
11console.log("sync end");

Expected order:

text
1sync start
2sync end
3microtask
4timer

An await continuation behaves like the promise continuation above. That is why code after await usually runs before setTimeout(..., 0) callbacks that were scheduled earlier in the same turn.

Sequential Versus Parallel Work

Inside an async function, two back-to-back await expressions create sequential execution. The second operation does not start until the first one completes.

javascript
1async function sequential() {
2  const a = await delay(100, "A");
3  const b = await delay(100, "B");
4  return [a, b];
5}

If the operations are independent, start them first and await them later.

javascript
1async function parallel() {
2  const aPromise = delay(100, "A");
3  const bPromise = delay(100, "B");
4  return Promise.all([aPromise, bPromise]);
5}

Both versions preserve the order of results in the returned array, but the second version finishes faster because the delays overlap.

That distinction explains why await often feels sequential: it preserves source-code order inside one function unless you explicitly create concurrency.

Why Business Logic Still Has Deterministic Order

If one statement appears after another inside an async function, JavaScript will not reorder those statements. The function may pause between them, but when it resumes, the remaining code continues in the original lexical order.

This gives you predictable control flow for dependent operations:

javascript
1async function createUserAndLog() {
2  const user = await Promise.resolve({ id: 7, name: "Ana" });
3  console.log("created", user.id);
4  await Promise.resolve();
5  console.log("audit written");
6}
7
8createUserAndLog();

You still need to think about concurrent tasks that were started elsewhere, but within one async function the order of the statements themselves remains stable.

Error Flow and Early Exit

Rejected promises also affect execution order. If an awaited promise rejects, control jumps to the nearest catch, and statements after the failing await are skipped.

javascript
1async function run() {
2  try {
3    console.log("step 1");
4    await Promise.reject(new Error("boom"));
5    console.log("step 2");
6  } catch (err) {
7    console.log("caught", err.message);
8  }
9  console.log("step 3");
10}
11
12run();

Output:

text
step 1
caught boom
step 3

Understanding this control transfer is important when later statements seem to “run out of order” but were actually skipped.

Common Pitfalls

A common mistake is assuming await blocks the entire JavaScript environment. It only pauses the current async function, so other event-loop tasks can continue.

Another issue is accidentally serializing independent operations with one await after another. If the tasks do not depend on each other, start them concurrently and join them with Promise.all.

Developers also often use Array.prototype.forEach with an async callback and expect sequential behavior. forEach does not await each callback. Use for...of for sequential async loops.

Finally, remember that timers and promise continuations are scheduled differently. Code after await often runs before a zero-delay timer because microtasks are processed before the next macrotask.

Summary

  • 'await pauses the current async function, not the whole thread.'
  • The remainder of the function resumes through the promise microtask queue.
  • Synchronous code runs first, then microtasks, then timer callbacks.
  • Back-to-back await calls are sequential unless you start the work in parallel first.
  • Most ordering bugs come from misunderstanding the event loop rather than from async and await themselves.

Course illustration
Course illustration

All Rights Reserved.