Course topics

By WebNest Studio Engineering Team

JavaScript Tutorial

The JavaScript Event Loop

JavaScript runs on a single thread, meaning it can only execute one piece of code at a time. Yet JavaScript programs routinely handle timers, network requests, and user interactions that take time to complete, without freezing. The event loop is the mechanism that makes this possible, coordinating the call stack with queues of pending work.

Understanding the event loop is essential for predicting the actual order in which asynchronous code runs — especially the surprising fact that a setTimeout with a delay of 0 milliseconds still runs after all currently pending synchronous code, no matter how quick that delay sounds.

Why it matters

JavaScript runs on a single thread, yet it handles timers, clicks and network responses without freezing. The event loop is how. It explains why a setTimeout of zero does not run immediately and why a long calculation locks the page, and predicting the order of output is a classic interview exercise.

The Call Stack

The call stack is where JavaScript keeps track of function calls currently in progress. When a function is called, a new frame is pushed onto the stack; when it returns, its frame is popped off. Because there is only one call stack, JavaScript can only run one function to completion at a time — this is what "single-threaded" means in practice.

Web/Runtime APIs and the Queues

Asynchronous operations like setTimeout, network requests, and DOM events are not handled directly by the JavaScript engine itself — they are handed off to APIs provided by the runtime environment (the browser or Node.js). When those operations complete, their callback functions are not run immediately; instead, they are placed into a queue to wait until the call stack is empty.

  • Macrotask (task) queue — holds callbacks from sources like setTimeout, setInterval, and I/O events.
  • Microtask queue — holds callbacks from Promises (.then/.catch/.finally) and queueMicrotask; it has higher priority than the macrotask queue.

How the Event Loop Coordinates Everything

The event loop continuously checks one simple condition: is the call stack empty? If it is, the event loop first drains the entire microtask queue (running every pending Promise callback, even ones queued by other microtasks), and only then takes the single oldest task from the macrotask queue and pushes it onto the call stack for execution. This cycle repeats indefinitely, which is why microtasks (Promises) always run before the next macrotask (like a setTimeout callback), even if the setTimeout was scheduled first.

Why setTimeout(fn, 0) Still Runs After Synchronous Code

Calling setTimeout(fn, 0) does not run fn immediately — it schedules fn as a macrotask to run after at least 0 milliseconds, but crucially, only once the call stack is completely empty. Since all currently executing synchronous code occupies the call stack right up until it finishes, and the event loop will not even glance at the task queue while the stack is non-empty, every line of synchronous code (and every pending microtask) runs to completion first, no matter how small the requested delay was.

Examples

Execution order proving synchronous code, then microtasks, then macrotasks

JavaScript
console.log("1: synchronous start");

setTimeout(() => {
  console.log("4: setTimeout (macrotask)");
}, 0);

Promise.resolve().then(() => {
  console.log("3: promise .then (microtask)");
});

console.log("2: synchronous end");
Output
1: synchronous start
2: synchronous end
3: promise .then (microtask)
4: setTimeout (macrotask)

Multiple microtasks always fully drain before the next macrotask runs

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

Promise.resolve()
  .then(() => {
    console.log("microtask 1");
    return Promise.resolve();
  })
  .then(() => console.log("microtask 2 (chained)"));

Promise.resolve().then(() => console.log("microtask 3"));

console.log("synchronous code");
Output
synchronous code
microtask 1
microtask 3
microtask 2 (chained)
macrotask

Common Mistakes

  • Assuming "setTimeout(fn, 0)" runs fn immediately, or before any synchronous code — it always waits for the call stack to clear first.
  • Assuming a setTimeout scheduled before a Promise will run first, without accounting for the microtask queue's higher priority over the macrotask queue.
  • Writing a long-running synchronous loop and being surprised that timers, network callbacks, and UI updates all appear "frozen" — nothing in either queue can run while the call stack is busy.
  • Believing JavaScript is multi-threaded because it can "do multiple things at once" — the concurrency comes from the runtime's APIs and queues, not from multiple threads executing JavaScript simultaneously.

Exercise

Replace each 0 placeholder with one of the numbers 1 to 4, each used once, so that the program prints them in ascending order. Work out the order from the rules: synchronous code first, then microtasks, then timers.

Expected output: 1, 2, 3, 4 (one per line)

JavaScript · starter code
console.log(0); // TODO

setTimeout(() => console.log(0), 0); // TODO

Promise.resolve().then(() => console.log(0)); // TODO

console.log(0); // TODO
Show hints
  • The two plain console.log calls run first, in the order they are written.
  • A promise callback is a microtask and runs before a timer callback, even a timer of 0 milliseconds.
Show solution
JavaScript · solution
console.log(1);

setTimeout(() => console.log(4), 0);

Promise.resolve().then(() => console.log(3));

console.log(2);

Quick quiz

1. How many things can JavaScript execute at the same moment on its main thread?

  • A. One
  • B. Two
  • C. Unlimited
  • D. As many as there are CPU cores
Show answer

A. There is a single call stack. Waiting is handed to the browser or to Node.js.

2. Which runs first once the current synchronous code has finished?

  • A. Timer callbacks
  • B. Microtasks, such as promise callbacks
  • C. Click handlers
  • D. Whichever was created first
Show answer

B. The whole microtask queue is emptied before the next task is taken.

3. What happens to the page while a long synchronous loop is running?

  • A. The loop runs in the background
  • B. It keeps working normally
  • C. It cannot respond to clicks or repaint until the loop ends
  • D. The browser splits the loop across threads
Show answer

C. Nothing else can use the call stack until the current code finishes.

Interview questions

How does the event loop work?

Show answer

JavaScript has one call stack and runs one piece of code at a time. Operations that wait, such as timers and network requests, are handled outside the stack by the browser or Node.js; when one completes, its callback is placed in a queue. The event loop checks whether the stack is empty, runs all pending microtasks, and then takes the next task from the task queue and runs it to completion. Then it repeats.

What is the difference between microtasks and macrotasks?

Show answer

Microtasks are callbacks of promises, including the code after an await, and those queued with queueMicrotask. Macrotasks, also called tasks, are timer callbacks, user events and I/O callbacks. After each task finishes, the entire microtask queue is drained before the next task starts or the page is rendered. That is why a resolved promise's callback runs before a setTimeout of zero.

Key Points to Remember

  • JavaScript executes on a single call stack, so only one function runs to completion at a time.
  • Asynchronous work is handled by runtime APIs, which queue callbacks rather than running them immediately.
  • The microtask queue (Promises) is always fully drained before the event loop picks the next macrotask (setTimeout, etc.).
  • setTimeout(fn, 0) only guarantees fn runs after the current call stack is empty and all pending microtasks finish — not that it runs instantly.

Practice in the playground

Write and run your own code for this topic, or start from the exercise or an example. Shows a live preview of your HTML, CSS and JavaScript. Your code for each topic is kept in this browser.

Run the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

WebNest Studio

Written by

WebNest Studio Engineering Team

Practical lessons from developers working with Java, Python, React, databases and production systems.

Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering