The event loop in JavaScript

JavaScript runs one piece of code at a time. The event loop decides when queued callbacks may enter the call stack.

The moving pieces

Call stack

Runs synchronous JavaScript, one function frame at a time.

Host runtime

Handles timers, network requests, user input, and other work outside the stack.

Task queue

Holds timer, event, and message callbacks ready for a future turn.

Microtask queue

Holds Promise reactions and queueMicrotask callbacks.

The rule that predicts the output

Run the current script or task to completion. When the stack is empty, drain every microtask. The browser may render, then the event loop can start the next task.

one task run stack drain microtasks browser may render

Microtasks go first after the current synchronous work, even if a timer with a delay of 0 is ready.

Trace one example

console.log("A");

setTimeout(() => {
  console.log("B");
}, 0);

Promise.resolve().then(() => {
  console.log("C");
});

console.log("D");

Output

  1. A
  2. D
  3. C
  4. B
  1. The script starts as a task.

    A prints synchronously.

  2. The timer starts.

    Its callback will enter the task queue when the timer is eligible. It does not enter the stack yet.

  3. .then schedules a microtask.

    The Promise is already fulfilled, but its reaction still runs later.

  4. D prints synchronously.

    The script finishes and the call stack becomes empty.

  5. The engine drains microtasks.

    C prints before another task can begin.

  6. The timer callback becomes the next task.

    B prints.

Promises contain synchronous and asynchronous parts

The function passed to new Promise runs immediately. A callback passed to .then runs as a microtask.

console.log("start");

new Promise(resolve => {
  console.log("executor"); // synchronous
  resolve();
}).then(() => {
  console.log("then");     // microtask
});

console.log("end");

// start, executor, end, then

await pauses one async function

Code before the first await runs synchronously. The rest of that async function resumes in a microtask after the awaited value settles. The thread itself is not blocked.

async function run() {
  console.log("inside 1");
  await 0;
  console.log("inside 2");
}

console.log("outside 1");
run();
console.log("outside 2");

// outside 1, inside 1, outside 2, inside 2

setTimeout(0) does not mean now

The delay is the minimum wait before the callback becomes eligible for a task. Existing synchronous work and all queued microtasks still get their turn first. Browsers may also clamp short nested timers.

setTimeout(() => console.log("timer"), 0);

const end = Date.now() + 200;
while (Date.now() < end) {
  // blocks the call stack for about 200 ms
}

console.log("loop finished");
// "loop finished" appears before "timer"

Long work still blocks the page

The event loop provides concurrency, not parallel execution of JavaScript on the same thread. A long task delays input handlers, timers, rendering, and queued Promise callbacks.

  • Split heavy work into smaller tasks.

    Yield between chunks when the interface needs to stay responsive.

  • Use a Web Worker for CPU-heavy work.

    A worker runs JavaScript on another thread and communicates through messages.

  • Do not create an endless microtask chain.

    The browser drains microtasks before moving on, so continuously adding more can starve tasks and rendering.

Check your understanding

  1. Which runs first after synchronous code: a resolved Promise's .then callback or setTimeout(..., 0)?
    Show answer

    The .then callback. It is a microtask, and the event loop drains microtasks before starting the next task.

  2. Does await block all JavaScript on the page?
    Show answer

    No. It pauses that async function. Other synchronous code continues, and the function resumes later in a microtask.

  3. Why might a zero-delay timer run much later than zero milliseconds?
    Show answer

    The current task must finish, the microtask queue must drain, and other scheduling delays may apply before the timer callback gets a turn.