Bitsy Explains
Home/JavaScript, Explained Like You're Five/Lesson 20

The Event Loop in JavaScript

JavaScript does one thing at a time, yet handles timers and network requests without freezing. The event loop is how. Learn why setTimeout 0 runs last and promises go first.

Lesson 20 of 203 min readVideo
Video thumbnail: The Event Loop Watch the 90-second video
Explain like I'm five

A restaurant has only one waiter. The waiter takes an order to the kitchen and doesn't wait there. They go help other tables. When the food is ready, the waiter picks it up and serves it. One waiter, lots of tables.

Guess the order

Read this and guess what prints.

console.log("1"); // prints: 1
setTimeout(() => console.log("2"), 0); // prints: 2
console.log("3"); // prints: 3

It prints 1, then 3, then 2. Even with a delay of zero, setTimeout sends its function to the back of the line. JavaScript finishes what it's doing first, then checks the line.

The zero means "as soon as possible after the current code", not "right now". The delay is a minimum wait, never an exact time.

The pieces

Here is how the restaurant matches the code.

  • Call stack: the waiter's hands. It holds the code that is running right now. JavaScript runs one thing at a time, so there is only one stack.
  • The browser (or Node): the kitchen. Timers, fetch requests and file reading are handed over here. The browser does the waiting, not JavaScript.
  • Queue: the counter where finished food waits. When a timer ends or data arrives, its callback is placed in the queue.
  • Event loop: the waiter checking the counter. Whenever the call stack is empty, it takes the next callback from the queue and runs it.

That's why the example printed 1, 3, 2. The timer's callback sat in the queue until all the code on the stack was done.

JavaScript has one waiter on purpose. In a browser, two pieces of code changing the same page at the same moment would make a mess. So JavaScript does one thing at a time, and sends slow jobs away.

The twist: the VIP line

Ready promises skip ahead of timers.

setTimeout(() => console.log("timeout"), 0); // prints: timeout
Promise.resolve().then(() => console.log("promise")); // prints: promise
console.log("sync"); // prints: sync

This prints sync, then promise, then timeout. Promise callbacks have their own VIP line, called the microtask queue. Once the current code is done, the event loop empties the whole microtask line before it takes anything from the timer line. Code after an await also goes in the microtask line.

Don't block the waiter

The waiter can only pick up food when their hands are free. If your code keeps the call stack busy, everything else waits, including timers that are already due and, in a browser, clicks and screen updates.

const start = Date.now();
setTimeout(() => console.log("late timer"), 0); // prints: late timer
while (Date.now() - start < 200) {}
console.log("busy loop done"); // prints: busy loop done

The timer asked for zero milliseconds, but it had to wait about 200, until the busy loop let go. Keep slow work out of the waiter's hands. Use the asynchronous versions of slow tasks, like fetch and timers, so the browser does the waiting while JavaScript keeps serving other tables.

Rule of thumb

Don't make JavaScript wait around. Send slow things to the kitchen, and they come back when they're ready.

Common mistake

Expecting setTimeout(fn, 1000) to run exactly one second later. The delay is a minimum, and the callback only runs once the call stack is empty and any promise callbacks ahead of it are done.

Check yourself

What order does this print? setTimeout(() => console.log("A"), 0); console.log("B");