Read first

Today

How JavaScript does many things at once with only one thread. About 25 minutes: the lesson, then the puzzle.

The idea

JavaScript runs your code on one thread with one call stack. It can only do one thing at a time. When you start something slow (a timer, a network request, reading a file), JavaScript hands it to the browser or to Node, and keeps going. When the slow thing finishes, its callback is put in a queue. The event loop is the rule that decides when a waiting callback gets to run: only when the call stack is empty.

That one rule explains most “why did this print in that order?” questions.

How it really works

There are two kinds of queue, and they are not equal:

QueueWhat goes in itWhen it runs
Task queue (macrotasks)setTimeout, setInterval, UI events like clicks, network and file I/O callbacksOne task per turn of the loop
Microtask queuePromise callbacks (.then, .catch, .finally), the rest of an async function after await, queueMicrotaskAll of them, right after the current task

One turn of the loop in a browser:

  1. Take one task and run it until the stack is empty. Your whole script file counts as the first task.
  2. Run every microtask. If a microtask adds another microtask, that one runs too, in the same round. The queue must be completely empty before moving on.
  3. Maybe render: run requestAnimationFrame callbacks, recalculate styles and layout, paint. On a 60 Hz screen this chance comes about every 16.7 ms.
  4. Go back to step 1.

The key sentence: after every task, all microtasks run before the next task and before the next paint.

Two details interviewers like:

  • setTimeout(fn, 0) does not mean “now”. It means “put fn in the task queue after at least 0 ms”. If the stack is busy for 2 seconds, fn waits 2 seconds. Browsers also enforce a minimum of about 4 ms once timers are nested 5 levels deep.
  • await splits an async function in two. Everything before the first await runs immediately, like normal code. Everything after it is scheduled as a microtask once the awaited value is ready. Calling an async function returns a promise straight away.

Example

Predict the output before you open the answer.

console.log('A');
 
setTimeout(() => console.log('B'), 0);
 
Promise.resolve().then(() => console.log('C'));
 
queueMicrotask(() => console.log('D'));
 
(async () => {
  console.log('E');
  await null;
  console.log('F');
})();
 
console.log('G');

Now the reason this matters for real pages:

// Version 1: the page freezes forever
function spin() { Promise.resolve().then(spin); }
spin();
 
// Version 2: the page keeps working
function spin2() { setTimeout(spin2, 0); }
spin2();

Version 1 keeps adding microtasks while the microtask queue is being emptied, so step 2 never ends: no paint, no clicks, nothing. Version 2 adds tasks, and between two tasks the browser can paint and handle clicks. Same idea, very different result.

Traps

1. forEach does not wait for async callbacks.

const ids = [1, 2, 3];
ids.forEach(async (id) => {
  await save(id);
});
console.log('all saved'); // prints BEFORE any save finishes

forEach ignores the promises its callback returns. Use for (const id of ids) { await save(id); } to go one by one, or await Promise.all(ids.map(save)); to run them all at once and wait for all of them.

2. Long synchronous code blocks everything. A loop that takes 3 seconds blocks timers, clicks and painting for 3 seconds, because nothing else can run until the stack is empty. Split the work into chunks with setTimeout, or move it to a Web Worker (a real second thread).

3. Node.js has extra queues. process.nextTick callbacks run before promise microtasks. In the main script, the order of setTimeout(fn, 0) and setImmediate(fn) is not guaranteed; inside an I/O callback, setImmediate always runs first. Since Node 11, microtasks run after each individual timer callback, the same as in browsers.

4. Promises are not threads. new Promise(resolve => { /* heavy loop */ }) still runs the heavy loop right now, on the main thread. A promise changes when the result is delivered, not where the work happens.

How interviews ask it

Build it

In a React or Next.js project, add a test page with a number that counts up using requestAnimationFrame, and a button that runs a 2-second busy loop. Click it and watch the counter freeze. Then fix it two ways: split the loop into chunks with setTimeout, and move it into a Web Worker. You will have seen the event loop instead of only reading about it, and it makes a good short answer to “tell me about a performance problem”.

Check yourself

Puzzle

Today’s puzzle is Two Sum: think about it away from the keyboard, then read the hint. The solution arrives tomorrow.

Market word: Java

Java was the most-asked skill in this week’s Canadian entry-level job posts that this wiki doesn’t cover yet (58 of 1,200 posts), mostly at banks and large companies. It is statically typed and compiles to bytecode that runs on the JVM. If you know C#, it is a short step: classes, interfaces, generics and garbage collection work almost the same way, and Spring Boot plays the role ASP.NET Core plays for C#.