BugForge
All lessons
Node.js · 01/19 6 min

The event loop, visually

Call stack, microtasks, macrotasks: why Promise callbacks beat setTimeout(0).

1console.log("A");
2setTimeout(() => console.log("B"), 0);
3Promise.resolve().then(() => console.log("C"));
4console.log("D");
console
call stack
main()
microtasks (promises)
macrotasks (timers)
1/8The script itself is the first task. It runs top to bottom on the call stack.

JavaScript runs one thing at a time on the call stack. Everything asynchronous is a callback that waits in a queue until the stack is empty.

The rules

  1. Run the current script to completion (synchronous code first, always).
  2. Drain all microtasks: Promise.then, await continuations, queueMicrotask.
  3. Take one macrotask: a setTimeout, I/O callback or setImmediate.
  4. Drain microtasks again. Repeat.

So the output of the classic puzzle above is A, D, C, B: synchronous logs first, then the promise callback, then the timer, even with a 0 ms delay.

Why debugging rounds care

  • Forgotten await: the function returns a pending Promise. if (promise) is always truthy, so a failed password check "passes".
  • array.forEach(async x => …) doesn't wait: forEach ignores the returned promises. Use for…of with await, or await Promise.all(array.map(…)).
  • Sequential awaits in a loop are slow when the calls are independent; Promise.all runs them concurrently.
  • Unhandled rejections: an async Express handler that throws without try/catch never reaches your error middleware in Express 4.
// ❌ returns before any save finishes
items.forEach(async (it) => { await it.save(); });
// ✅
await Promise.all(items.map((it) => it.save()));
Practise it now
Next in Node.js
Express request pipeline