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,
fetchrequests 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.
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
setTimeout(() => console.log("A"), 0); console.log("B");The setTimeout callback goes to the queue and only runs after the current code, including the log of B, has finished.