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

Lesson 05: Predict it, then prove it

Lesson objectives:

  • Apply a four-step procedure to predict the output order of any mixed async snippet.
  • Predict the exact order of a snippet combining sync logs, setTimeout, .then(), and await.
  • Verify your prediction by running the snippet in a real Node or browser console and explain each line by the model.

Prerequisites: Lessons 01-04 (call stack, task queue, event loop, microtask queue, await) | Previous << 04

From "I think it's roughly this" to "I know, and here's why"

You now know every rule: run-to-completion, the task queue, the event loop, the microtask queue and its drain-first priority, and what await does. But knowing the rules and applying them under pressure to a tangled snippet are different skills. Faced with six lines mixing timers, promises, and awaits, it is easy to freeze. This lesson gives you a fixed procedure so you never have to freeze — you just run the steps — and then you prove the answer in a real console instead of trusting your gut. Calling the order and being right, repeatably, is the whole point of the course.

Explanation

Here is a procedure that works on any snippet. Do these four steps in order, on paper.

  1. Do the synchronous pass. Read top to bottom. Every plain line runs now, and an async function runs now up to its first await. Write down what prints, in order. As you pass each deferred thing, note it: a setTimeout callback joins the task queue; a .then()/.catch()/.finally() callback and the continuation after an await join the microtask queue12.
  2. Drain all microtasks. The synchronous pass is done and the stack is empty, so run the entire microtask queue in scheduling order, and keep going if microtasks schedule more microtasks3. Write down what prints.
  3. Run one task. Take the single oldest callback from the task queue and run it1.
  4. Repeat 2-3. After that task, drain microtasks again, then run the next task, until both queues are empty.

The one rule that ties it together: microtasks always drain before the next task1. Steps 2 and 3 encode it — you never run a second task without first emptying the microtask queue. Keep the two queues as two separate lists on your page and this stays mechanical.

Now apply the procedure to the snippet below before revealing the answer.

If your call was right, you did not guess — you ran a procedure. If it was off, the miss is almost always in step 2 versus step 3: letting a task run before the microtask queue is empty. That single boundary is where the "wrong order" surprises of this whole course come from.

Worked example (follow along)

Apply the four steps out loud on this snippet:

Step 1 — synchronous pass. console.log("A") prints A. The setTimeout line queues callback B in the task queue. go() runs synchronously up to its await: it prints C, then suspends and queues its continuation D in the microtask queue. The .then() line queues E in the microtask queue (after D). console.log("F") prints F. Synchronous output so far: A, C, F. Task queue: [B]. Microtask queue: [D, E].

Step 2 — drain all microtasks. Stack is empty. Run D (prints D), then E (prints E). Microtask queue is now empty.

Step 3 — run one task. Take B from the task queue and run it (prints B).

Step 4 — repeat. Both queues are empty, so we stop. Final order: A, C, F, D, E, B. Every step came from the procedure, not intuition.

Your turn (faded example)

Run the four steps yourself. Keep a "task queue" list and a "microtask queue" list as you go, then fill in the order:

Answer: the order is x, p, r, z, q, y. Step 1 (synchronous pass): x prints; setTimeout queues task y; the .then() queues microtask z; m() runs to its await, printing p and queueing microtask q; r prints. So synchronous output is x, p, r; task queue [y]; microtask queue [z, q] (the .then() was scheduled before m() ran, so z is ahead of q). Step 2 drains microtasks in order: z, then q. Step 3 runs the one task: y. The .then() on line 3 prints before m()'s await continuation because it was scheduled first — within a queue, order is scheduling order.

Summary + what's next

You have a procedure, not a hunch: do the synchronous pass (async functions run up to their first await), drain the entire microtask queue, run one task, repeat — and microtasks always drain before the next task. That single machine — stack, then all microtasks, then one task — explains every "wrong order" you started this course confused by. The console.logs were never out of order; you were reading them against the wrong model. Now you read them against the right one, and you can prove it in any console. One scope note: this is the browser/generic event-loop model, and it is the one that makes sync-vs-microtask-vs-task ordering predictable; Node layers extra machinery on top — process.nextTick (which outranks the promise microtask queue) and the libuv phases (timers / I/O / setImmediate) — that can reorder things this model does not cover, and that is out of scope here. Keep the two-list habit: whenever an ordering surprises you, write the task queue and microtask queue side by side and walk the four steps.

Footnotes

  1. MDN: JavaScript execution model — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Execution_model 2 3

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

  3. MDN: Using microtasks in JavaScript with queueMicrotask() — https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide

Exercises

Exercise 1 · Level 1 (warm-up)

Paste the capstone snippet (the 1 through 6 one from the prediction block) into capstone.mjs. Predict the order with the four-step procedure first, then run node capstone.mjs and confirm it matches your written prediction line for line.

Done criteria · checked locally
Exercise 2 · Level 2 (capstone — predict, run, and explain)

In your own editor, write a fresh snippet named myorder.mjs that includes all of these, in any arrangement you choose: at least two synchronous console.logs, one setTimeout(fn, 0), one .then() on a resolved promise, and one async function with an await and a log after it. Then, on paper or in a comment:

  1. Predict the full output order using the four-step procedure, writing a short reason next to each line (synchronous / microtask / task).
  2. Run it with node myorder.mjs (or in the browser console) and compare against your prediction.
  3. If any line differs, find the first divergence and write one sentence explaining the rule you missed.
Done criteria · checked locally