Here's a small JavaScript puzzle. What order do these letters print in?
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
async function run() {
console.log("D");
await null;
console.log("E");
}
run();
console.log("F");
Take a guess before you scroll.
.
.
.
The answer is:
A D F C E B
B has a delay of 0, but it prints last. To see why, you need to know four things.
The four pieces
The call stack holds the code running right now. JavaScript runs one piece of code at a time on the main thread, and it finishes the current job before starting another.
Host APIs are provided by the browser or Node, not the JavaScript language. Timers are one example. They wait outside the call stack, so waiting doesn't block your code.
The macrotask queue (also called the task queue) holds jobs such as timer callbacks. The event loop takes one at a time.
The microtask queue holds jobs such as Promise callbacks and the code that continues after an await. It's drained completely before the next macrotask runs.
The event loop is the rule that connects them:
- Run the current task until the call stack is empty.
- Run every microtask, including any new ones added along the way.
- Take one macrotask and run it.
- Repeat.
Walking through the code
Step 1: console.log("A"). This is synchronous, so it prints right away.
Output: A
Step 2: setTimeout(..., 0). The timer is handed to the host. Its callback, which prints B, can't run until the current code has finished. It goes into the macrotask queue when the timer expires.
Step 3: Promise.resolve().then(...). The Promise is already fulfilled, so its callback, which prints C, goes straight into the microtask queue. It doesn't run yet, because .then callbacks never run synchronously.
Step 4: run(). Calling an async function runs its body synchronously until the first await. So D prints immediately.
Output: A D
Step 5: await null. This is the part that surprises people. await pauses run and queues the rest of the function, the line that prints E, as a microtask. Control goes back to the code that called run().
Step 6: console.log("F"). This is still the original script, so it prints now.
Output: A D F
Step 7: the call stack is empty. Now the microtask queue is drained, in order. C was queued first, then E.
Output: A D F C E
Step 8: one macrotask runs. The timer callback finally gets its turn.
Output: A D F C E B
What setTimeout(fn, 0) really means
It doesn't mean "run this now." It means "run this in a later task, as soon as possible." The delay is a minimum, not a guarantee.
That matters in real code. If the call stack stays busy, or a long chain of microtasks keeps adding more microtasks, the timer has to wait. In a browser, the page can't repaint during that time either, which is how a page ends up frozen.
Three things to remember
- Synchronous code always finishes first.
- All microtasks run before the next macrotask.
- Code before the first
awaitin an async function runs synchronously. Code after it doesn't.
See it run
Reading this is one thing, but it clicked for me when I could watch the queues change. I built a step-through version of this exact example, where you can watch the call stack, both queues and the console at every step:
It's part of Software Atlas, a free site I'm building with interactive explanations of how software works. It has no ads and no sign-up.
Top comments (4)
One detail worth adding: the timer delay is a floor, not a promise. Browsers clamp nested timers to about 4 ms and throttle background tabs, so setTimeout(0) only guarantees not before the current task and its microtasks. Microtasks queued while the microtask queue is draining run in the same checkpoint, which is why an await chain that never yields to a macrotask can starve rendering.
Good point, thanks. Yeah the 4ms clamp only kicks in after about 5 levels of nesting, so the first few timers can fire sooner. Background tabs get way worse though.
The await chain one is sneaky. Looks async, but nothing renders until it hands control back.
The step by step walk through the await pause is the part that clarifies the order, because it shows E becoming a microtask rather than running inline. Do you think a long chain of awaited steps could delay a timer noticeably in real code, or does the microtask drain usually finish quickly enough that setTimeout(0) stays close to immediate?
In real code probably not. Awaits are super cheap, you'd need a crazy long chain before a timer noticed.
The problem is usually what's between them. Heavy work between already resolved awaits blocks timers and rendering, because nothing else gets a turn. Real I/O like fetch doesn't have this problem.