JavaScript async/await: Why Your console.logs Print in the "Wrong" Order · 第 4 / 5 节

Lesson 04: await is .then() in disguise

Lesson objectives:

  • Explain that an async function runs synchronously up to its first await.
  • Explain that the code after an await runs as a microtask, not synchronously.
  • Judge correctly that await suspends only its own function and never blocks the thread.

Prerequisites: Lesson 03 (the microtask queue and its drain-first rule) | Previous << 03 | Next 05 >>

"await" reads like "wait here" — so does the whole program freeze?

The word await is a trap for your intuition. It looks like it means "stop everything and wait," and so people picture the entire program freezing on that line until the promise resolves. If that were true, one slow await would lock up the page. It doesn't — the UI keeps responding, other code keeps running. So await cannot mean "freeze the thread." What it actually does is the last missing piece of your model, and it reuses machinery you already know from Lesson 03: it turns everything after it into a microtask.

Explanation

Start with what runs before the first await. An async function is not special up to that point — it runs synchronously, right now, on the call stack, just like any function1. An async function with no await at all runs entirely synchronously; the "async" only starts to matter at the first await1.

Now the await itself. It pauses the execution of its own async function until the awaited promise settles2. But "pauses" does not mean "blocks the thread." Control exits the function and returns to the caller, which keeps running the rest of the program; when the awaited value is ready, a microtask that continues the paused function gets scheduled2. In other words, await splits your function in two: everything up to the await runs now, and everything after it becomes a microtask that runs later — exactly like putting the rest of the function inside a .then().

This is why await never freezes anything. The await expression never blocks the main thread; it only defers the code that depends on the result — the lines after the await2. And this holds even when the promise is already resolved: the function still pauses until the next tick, and the caller resumes in the meantime2. So the code after an await is always deferred to a microtask, never run inline.

Trace it against the rule. console.log("start") prints start. Then greet() is called and runs synchronously until its await: it prints A. At the await, greet suspends, console.log("B") becomes a microtask, and control returns to the caller. Back in the main script, console.log("end") prints end. Now the stack is empty, the event loop drains the microtask queue, and the continuation runs, printing B. The order is start, A, end, BA is synchronous (before the await), but B waited in the microtask queue.

Because the code after await is just a scheduled microtask, everything from Lesson 03 applies unchanged: an await continuation drains before the next setTimeout task, and it interleaves with other microtasks in scheduling order. await did not add a new rule — it fed your function into the microtask queue you already understand.

Worked example (follow along)

Two async calls and a synchronous log, to see where each half of a function lands:

Split each function at its await. first() runs synchronously up to its await: it prints first: before await, then suspends and schedules its continuation (microtask 1). Control returns, and second() runs synchronously up to its await: it prints second: before await, then suspends and schedules its continuation (microtask 2). Then console.log("synchronous end") prints. Now the stack is empty and the microtask queue drains in scheduling order: first: after await (microtask 1), then second: after await (microtask 2). The order is: first: before await, second: before await, synchronous end, first: after await, second: after await. Every "before await" is synchronous; every "after await" is a microtask.

Your turn (faded example)

Predict the output. Mark each line "sync" (before an await) or "microtask" (after an await). Fill in the order:

Answer: the order is 3, 1, 4, 2. Walk it: console.log("3") is synchronous and prints 3. run() runs synchronously up to its await, printing 1, then suspends and schedules console.log("2") as a microtask; control returns to the caller. console.log("4") is synchronous and prints 4. The stack is now empty, so the microtask runs and prints 2. The two logs inside run() land on opposite sides of the await: 1 is synchronous, 2 is a microtask — so the synchronous 4 from the caller slips in between them.

Summary + what's next

An async function runs synchronously up to its first await; at the await, it suspends only itself, hands control back to the caller, and schedules the rest of the function as a microtask. await never blocks the thread — it defers the lines after it, exactly like wrapping them in .then(). That is the last mechanism. You now hold every rule in this model: the stack runs synchronous code, then the microtask queue drains completely, then one task runs, and repeat — and await just feeds continuations into the microtask queue. In Lesson 05 you turn these rules into a repeatable procedure and use it to call the output of a genuinely tangled snippet, then prove your call in a real console.

Footnotes

  1. MDN: async function — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function 2

  2. MDN: await — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await 2 3 4

Exercises

Exercise 1 · Level 1 (warm-up)

Paste the greet() snippet from the Explanation into await.mjs (or the console). Predict the order, then run node await.mjs and compare.

Done criteria · checked locally
Exercise 2 · Level 2 (advanced)

Add a setTimeout(() => console.log("timer"), 0) as the first line of the greet() snippet. Predict exactly where timer prints relative to B, then verify.

Done criteria · checked locally