Profile    Mohammed Shiroz Status   Loading  
Logo
Share This
Back to blog
Filter by:
Tags
//Article title

The JavaScript Event Loop Explained: Why setTimeout(0) Isn't Instant

About Post

Quick quiz before we start. What does this print?

console.log('A');

setTimeout(() => console.log('B'), 0);

Promise.resolve().then(() => console.log('C'));

queueMicrotask(() => console.log('D'));

console.log('E');

If you said A, E, C, D, B, you can probably skim this. If you expected B to come earlier, because the timeout is literally zero, you're in very good company. And once you see why it's last, a whole category of "why is this running in the wrong order?" bugs stops being mysterious.

The kitchen with one chef

JavaScript runs your code on a single thread. Picture a kitchen with exactly one chef. The chef can only do one thing at a time, but the kitchen still serves many tables, because the slow parts (the oven, the delivery driver, the fridge) don't need the chef standing next to them.

That's the whole trick. Four pieces make it work:

  • The call stack is what the chef is doing right now. Functions are pushed on when called and popped off when they return.
  • The environment (the browser's Web APIs, or libuv in Node) is the oven and the timers. It handles waiting: network requests, timers, file reads. Not your JavaScript.
  • The task queue (also called the macrotask or callback queue) is the line of order tickets: "the timer finished", "the click happened", "the response arrived".
  • The microtask queue is a small stack of urgent notes stuck to the chef's station: promise callbacks, await continuations and queueMicrotask().

The loop itself

The event loop is the chef's routine, repeated forever:

  1. Take one task from the task queue and run it until the call stack is empty.
  2. Run every microtask, including any new microtasks added while doing so, until the microtask queue is empty.
  3. In a browser, render if it's time to update the screen.
  4. Go back to step 1.

The important asymmetry: one task at a time, but all the microtasks at once. Microtasks always jump the queue ahead of the next task.

Back to the quiz

Now the order makes sense:

  1. The whole script is itself a task. It logs A.
  2. setTimeout hands a timer to the environment. When it fires, its callback joins the task queue.
  3. The .then() callback and queueMicrotask go into the microtask queue.
  4. The script logs E and finishes. The call stack is empty.
  5. Microtasks run, in order: C, then D.
  6. Only now does the loop pick the next task: the timer callback, B.

await follows the same rule. Everything after an await is a microtask, which is why this prints 1, 3, 2:

async function load() {
  console.log('1');
  await null;
  console.log('2'); // continues in a microtask
}

load();
console.log('3');

Why setTimeout(fn, 0) is not instant

The delay is a minimum, not a promise. "Zero" really means "put this in the task queue as soon as possible". The callback still waits for:

  • the current code to finish,
  • every pending microtask,
  • any tasks already ahead of it in the queue.

On top of that, browsers clamp deeply nested timers (a timeout scheduled from a timeout, several levels deep) to at least 4 ms, and they throttle timers heavily in background tabs. Node treats a delay of 0 as 1 ms. So setTimeout(fn, 0) is a useful way to say "after the current work", never a way to say "now".

The two ways to break the loop

Block the stack. While your code runs, nothing else can: no clicks, no rendering, no other callbacks. In a browser, this freezes the page:

button.addEventListener('click', () => {
  const end = Date.now() + 3000;
  while (Date.now() < end) {} // three seconds of frozen page
});

In Node, the same mistake freezes every request your server is handling, not just one. Heavy CPU work belongs in a Web Worker, a worker thread or a queue job, or should be split into chunks that yield back to the loop between pieces.

Starve it with microtasks. Because the loop drains all microtasks before moving on, a microtask that keeps scheduling another microtask means the next task, and the next render, never comes. It's rarer, but it's the same freeze wearing a different hat.

The mental model in one line: one task, then all the microtasks, then (in the browser) maybe a render, repeat. Promises and await are microtasks; timers, events and I/O callbacks are tasks.

Node.js: same idea, extra details

Node has the same basic loop, built on libuv, with a few additions you'll meet sooner or later:

  • Phases. Node's loop cycles through phases: timers, I/O callbacks, a "check" phase for setImmediate, close callbacks and a few internal ones. Microtasks run between each callback.
  • process.nextTick() has its own queue that, in a CommonJS script, runs even before promise microtasks. Powerful, and easy to overuse.
  • setImmediate() vs setTimeout(fn, 0). From the main script, their order isn't guaranteed. Inside an I/O callback, setImmediate always runs first.
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));

// CommonJS: nextTick, promise, then timeout/immediate in either order

One more gotcha: in an ES module, top-level code already runs inside the promise machinery, so promise can print before nextTick. Yet another reason not to write code that depends on these fine-grained orderings.

What to take away

  • JavaScript runs one thing at a time; the environment does the waiting.
  • Microtasks (promises, await) always run before the next task (timers, events, I/O).
  • setTimeout(fn, 0) means "soon, after everything else", not "now".
  • Long synchronous work freezes everything. Move it off the main thread or split it up.
  • If your code depends on exact ordering between timers, promises and ticks, make the dependency explicit with await instead.

Which ordering surprised you the first time you hit it: the setTimeout zero, the await continuation, or something stranger?

Comments (0)
Leave your review

Thanks for your valuable comments. Your comments has been updated and appreciate your getting in touch...

01. About Shiroz

Mohammed Shiroz

Hi, I'm Mohammed Shiroz, a software engineer and AI enthusiast from Sri Lanka who turns ideas into intelligent, real-world solutions. With over 9 years of hands-on experience, I currently lead real estate ERP development at Kate Group, a...

03.My Projects

04. Categories

Ready To order Your Project ?

Get in Touch
Close