Skip to content

Intermediate · Asynchronous JavaScript · topic 40 of 61 · 30 min

The event loop

Explanation

The event loop is how one thread handles many async jobs. Your code runs on the call stack; finished async jobs put callbacks in queues. Whenever the stack is empty, ALL microtasks (promise callbacks) run first, then one macrotask (such as a setTimeout callback). This explains why a promise callback runs before a 0 ms timeout.

The pieces

  • Call stack: the functions currently running. JavaScript runs one thing at a time, top of the stack first.
  • Host APIs: the browser (timers, network, DOM events) or Node.js (libuv: files, network, timers). They do the waiting outside your code.
  • Macrotask queue (also called the task queue): callbacks of timers, I/O and events wait here.
  • Microtask queue: promise reactions (then/catch/finally callbacks, code after await) and queueMicrotask callbacks wait here.
  • Event loop: keeps checking: if the stack is empty, run all microtasks, then take the next macrotask, and repeat.

The order rule

  1. Run the current script (synchronous code) to the end.
  2. Run EVERY microtask in the queue, including new microtasks added while doing so.
  3. (In a browser, the page may be repainted here.)
  4. Take ONE macrotask (for example the oldest ready timer callback) and run it.
  5. Go back to step 2.

Predicting output

javascript
console.log('1 script start');
setTimeout(() => console.log('5 timeout'), 0);
Promise.resolve().then(() => console.log('3 promise 1'))
                 .then(() => console.log('4 promise 2'));
console.log('2 script end');
output
1 script start
2 script end
3 promise 1
4 promise 2
5 timeout
Synchronous code first, then all microtasks (both thens), then the timer macrotask.

await and the microtask queue

javascript
async function job() {
  console.log('B inside job');      // runs synchronously, up to the first await
  await null;
  console.log('D after await');     // a microtask
}
console.log('A');
job();
console.log('C');
// A, B inside job, C, D after await

An async function runs synchronously until its first await. The rest of the function continues later as a microtask, after the current synchronous code is done.

Why long loops freeze the page

While your code is running, the stack is not empty, so the event loop cannot run anything else: no clicks, no timers, no repainting. A loop that takes 3 seconds freezes the tab for 3 seconds. A setTimeout of 0 ms also waits until the stack is empty, so it can fire much later than asked.

javascript
const start = Date.now();
setTimeout(() => console.log('timer after', Date.now() - start, 'ms'), 0);
while (Date.now() - start < 200) {}  // busy for 200 ms
// timer after 200 ms (not 0!)
Careful: Endless microtasks also block: a promise callback that keeps scheduling another promise callback never lets the loop reach timers or repaint. Split heavy work into chunks with setTimeout, or move it to a Web Worker (browser) or worker_threads (Node).

Node.js details

Node has the same idea with a few extras: process.nextTick callbacks run even before promise microtasks, and setImmediate runs in a later phase than timers. For most code, "sync first, then microtasks, then macrotasks" is enough.

Think of it like this: The event loop is like a single bank cashier. He serves the person at the counter completely (the call stack). Between customers he first handles every quick "just sign here" request from staff waiting beside him (microtasks), and only then calls the next token from the queue (a macrotask).
Tip: Interview trick: write down three columns, Sync, Micro and Macro. Walk through the code once, putting each log in its column, then read the columns in that order.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

The event loop · 1/7

The event loop

In this lesson you will see how one thread handles many async jobs, using the event loop.

Prefer a video by a teacher? Find free videos on “javascript event loop microtask macrotask explained” on YouTube ↗

In real life: Why the loader never appeared

A developer showed a "Generating report…" message and then ran a 2-second calculation in the same function. Users never saw the message: the browser could not repaint until the stack emptied, and by then the report was done. Moving the heavy work into a setTimeout (or a Web Worker) let the browser paint the message first.

javascript
const log = [];
log.push('show loader');
setTimeout(() => { log.push('heavy work'); console.log(log); }, 0);
log.push('function ends; browser can repaint now');
Prints ['show loader', 'function ends; browser can repaint now', 'heavy work'].

Practice (0/3 done)

Task 1 · Question

What is the output order? console.log('a'); setTimeout(() => console.log('b')); Promise.resolve().then(() => console.log('c')); console.log('d');

Task 2 · Code · Javascript

Fill in the array `order` with the strings "sync", "micro" and "macro" in the order the three callbacks below actually run. Do not change the scheduling code.

Task 3 · Code · Javascript

Write yieldToEventLoop() that returns a promise resolving on the NEXT macrotask (use setTimeout 0). It is used to let the browser breathe during long work. The test checks that a promise microtask scheduled before it runs first.

Quick quiz

Question 1

1.Which queue do promise then callbacks use?

Question 2

2.When does a setTimeout(fn, 0) callback run?

Question 3

3.async function f() { console.log(1); await 0; console.log(3); } f(); console.log(2); Output?

Question 4

4.Why does a 3-second loop freeze a web page?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

Which queue do promise then callbacks use?

Answer: Microtask queue

Promise reactions are microtasks.

When does a setTimeout(fn, 0) callback run?

Answer: After the current code and all microtasks

Timers are macrotasks and wait for an empty stack and microtask queue.

async function f() { console.log(1); await 0; console.log(3); } f(); console.log(2); Output?

Answer: 1 2 3

f runs synchronously until await; the rest continues as a microtask after 2.

Why does a 3-second loop freeze a web page?

Answer: The stack never empties, so the event loop cannot handle clicks or repaint

Nothing else can run until the current code finishes.

console.log('A'); setTimeout(() => console.log('B'), 0); console.log('C'); What is printed?Related

Answer: A C B

Even with 0 ms, the timer callback runs after the current code finishes.

From Why async, and callbacks

What does this print? console.log('A'); setTimeout(() => console.log('B'), 0); Promise.resolve().then(() => console.log('C')); console.log('D');Related

Answer: A D C B

Normal code runs first (A, D). Then the microtask queue runs (C), and only then the timer task (B), even with 0 ms.

From Promises and the event loop

How do you handle a rejected await?Related

Answer: try/catch around the await

A rejection is thrown at the await, so catch it.

From async and await

What does an async function return?Related

Answer: A Future

async functions return Futures.

From Futures and async/await

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 1 more topic in “Asynchronous JavaScript” before its exam: fetch: talking to servers.
  2. Module 8 exam: Asynchronous JavaScript · 12 questions · 25 min, without help. 6 topics of this module still to finish first.
  3. Then the next module, TypeScript basics. 61 topics left in the course before the final exam.

← Previous: Promise.all, allSettled, race and any