· tips  · 6 min read

Understanding the Event Loop: Myths vs. Reality

Cut through the noise: learn what the JavaScript event loop actually does, why common claims (like “setTimeout(0) runs before promises”) are wrong, how browsers and Node differ, and how to reason reliably about async code.

Cut through the noise: learn what the JavaScript event loop actually does, why common claims (like “setTimeout(0) runs before promises”) are wrong, how browsers and Node differ, and how to reason reliably about async code.

What you’ll be able to do after reading this

You’ll be able to predict the order of async callbacks, explain why a promise’s .then runs before setTimeout(0), reason about UI updates and I/O, and choose the right primitives (setTimeout, requestAnimationFrame, microtasks, workers) for your needs. Read on and the event loop will stop feeling like magic and start feeling like leverage.


Quick outcome-first summary

The JavaScript event loop is a simple mental model with a few caveats: environments deliver tasks (macrotasks) and microtasks in well-defined queues; microtasks run immediately after the currently executing task completes and before the next task; browsers and Node.js implement details differently (timers, rendering, libuv phases, process.nextTick), which explains many myths. Once you learn the task vs microtask distinction and environment-specific quirks, you can predict behavior reliably.


Myth 1 - “JavaScript is single-threaded, so there’s no concurrency”

Reality: JavaScript code runs on a single main thread in many environments, yes. But concurrency exists. Browsers and Node.js provide a rich set of background threads (I/O, timers, networking, rendering, workers) that do work outside that main thread. The event loop coordinates the results.

Why the myth persists: people conflate single-threaded execution of JavaScript code with a single-threaded system overall.

Reality checklist:

  • The JS engine executes JS on one thread (in most common contexts). But system or runtime threads (e.g., network, disk, timers, rendering, Web Workers, Node worker_threads) perform other work off-thread.
  • The event loop schedules callbacks back onto the main thread once those background operations complete.

References: MDN’s explanation of the Concurrency model and the event loop.


Myth 2 - “setTimeout(fn, 0) runs immediately” or “setTimeout runs before Promise callbacks”

Reality: setTimeout(fn, 0) schedules a macrotask (task) that is executed only after the current task and all microtasks are drained. Promise callbacks (.then/.catch/.finally) are scheduled as microtasks and therefore run before the next macrotask.

Example (browser and Node):

console.log('start');
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('end');

// Output:
// start
// end
// promise
// timeout

Why: microtasks are drained after the current task finishes but before the event loop moves to the next task. That’s why promises win over setTimeout(0).

Good primer: Jake Archibald’s article on tasks, microtasks, queues and schedules.


Myth 3 - “Promises run synchronously when created” (or the converse claim)

Reality: The Promise executor function runs synchronously (the function you pass to new Promise). But handlers added with .then/.catch are asynchronous: they run as microtasks after the current synchronous code finishes.

Example:

new Promise(resolve => {
  console.log('executor');
  resolve();
}).then(() => console.log('then'));
console.log('after promise');

// Output:
// executor
// after promise
// then

So: executor synchronous, reaction (then) asynchronous (microtask).


Myth 4 - “The event loop is one single algorithm - same everywhere”

Reality: The concept of an event loop is shared, but implementations differ.

  • In browsers the event loop integrates with the rendering pipeline (reflow/paint), and has specific queues for timers, user interaction, rendering (requestAnimationFrame), etc.
  • In Node.js the loop is implemented by libuv and exposes phases (timers, pending callbacks, idle/prepare, poll, check, close), plus special queues such as process.nextTick and the microtask queue.

Crucial differences to be aware of:

  • Node has process.nextTick which runs before other microtasks and can starve I/O if abused.
  • Node’s setImmediate is a separate check-phase macrotask that often runs after I/O callbacks but before timers in certain conditions.

See Node’s official docs on the event loop and libuv.


Myth 5 - “Microtasks can’t starve rendering or I/O”

Reality: They can, indirectly.

Because microtasks are drained fully before moving to the next macrotask (and before rendering in browsers), a flood of microtasks can delay rendering and other macrotask work. In Node, process.nextTick callbacks are prioritized and can starve the I/O/poll phase. Be mindful: infinite microtask loops can hang the main thread.

Example of dangerous pattern:

function spin() {
  Promise.resolve().then(spin);
}
spin(); // will keep scheduling microtasks forever - the event loop never proceeds to tasks

This blocks rendering and other queued tasks.


Reality: A compact mental model that works

  1. Your JS code executes as one task. While it runs, it can synchronously schedule more things (promises, timeouts, events).
  2. When that sync code completes, the engine processes the microtask queue (Promise callbacks, MutationObserver callbacks, queueMicrotask). It runs all microtasks until empty.
  3. Then the event loop picks the next macrotask (e.g., timer callback, I/O callback, UI event handler) and runs it.
  4. In browsers, after appropriate macrotasks and microtasks, rendering may occur (layout/paint), requestAnimationFrame callbacks run in a render-related step, and then the cycle repeats.

Short: Task -> drain microtasks -> maybe render -> next task.

References: WHATWG HTML Standard on event loops and tasks and MDN.


Node.js special notes

  • process.nextTick runs before other microtasks (and can starve them). Use sparingly.
  • setImmediate exists as a check-phase callback and is not the same as setTimeout(fn, 0).
  • libuv phases: timers, pending callbacks, poll, check, close - many intricacies come from how these are scheduled.

Example demonstrating differences:

setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));

// In certain Node conditions, output could be:
// immediate
// timeout
// or the reverse, depending on whether the timers phase or check phase runs first.

See Node’s in-depth guide: Event loop, timers, and nextTick.


Practical rules you can use right now

  • If you need to schedule work after the current synchronous block but before the next render or I/O task: use a microtask (Promise.resolve().then(…), queueMicrotask).
  • If you need to schedule work on the next macrotask tick (allow rendering / other tasks to run first): use setTimeout(fn, 0) or setImmediate (Node) or postMessage trick for finer control.
  • For animation-related work use requestAnimationFrame (browser) - keeps you in step with rendering.
  • Avoid infinite microtask loops - they block the loop.
  • Use worker threads (Web Workers / Node worker_threads) for CPU-heavy tasks so they don’t block the main loop.

Short examples to remember

  1. Promise vs setTimeout
Promise.resolve().then(() => console.log('micro'));
setTimeout(() => console.log('macro'), 0);
console.log('sync');
// sync -> micro -> macro
  1. Promise executor vs then
new Promise(res => {
  console.log('exec');
  res();
}).then(() => console.log('then'));
console.log('after');
// exec -> after -> then
  1. Danger: microtask spin
(function loop() {
  Promise.resolve().then(loop);
})();
// never leaves microtask queue - starves rendering and tasks

Controversial ideas explained

  • “Promises are always async”: partly true. Executors run sync, handlers run async as microtasks.
  • “setImmediate is the same as setTimeout 0”: false. They belong to different phases (Node). Behavior can differ.
  • “process.nextTick is harmless”: false - it runs before other microtasks and can starve the loop.
  • “The browser event loop always yields for rendering between tasks”: mostly true, but heavy microtask sequences can delay rendering.

Debugging tips

  • Log the order with simple examples to see how your environment behaves.
  • Use browser devtools performance profiling to see long tasks and dropped frames.
  • In Node, inspect the event loop with diagnostic tools (e.g., 0x, clinic.js) to find blocking code.

Further reading


Final, essential point

The event loop is not a mystery - it’s a few queues and rules that all environments implement with small differences. Learn the task vs microtask divide and the environment-specific quirks; you will stop debugging surprises and start writing predictable async code. Remember: microtasks are powerful, but they can also silently starve the very things you rely on - use them with respect.

Back to Blog

Related Posts

View All Posts »
Microtasks vs Macrotasks: The Great JavaScript Showdown

Microtasks vs Macrotasks: The Great JavaScript Showdown

A deep dive into microtasks and macrotasks in JavaScript: what they are, how the event loop treats them, how they affect rendering and UX, illustrative diagrams and real-world examples, plus practical guidance to avoid performance pitfalls.

The Dangers of eval: A Cautionary Tale

The Dangers of eval: A Cautionary Tale

A deep dive into why eval and its cousins (new Function, setTimeout(string)) are dangerous, illustrated with real-world-style examples and concrete mitigations for web and Node.js applications.

Destructuring Function Arguments: A Cleaner Approach

Destructuring Function Arguments: A Cleaner Approach

Learn how to simplify and clarify JavaScript function signatures with parameter destructuring. See side-by-side examples, real-world patterns, pitfalls, and TypeScript usage to write more readable, maintainable code.