Event loop in JavaScript: how a single thread runs asynchronous code

The event loop orchestrates a single-threaded language: call stack, task queue, microtask queue, and what happens the moment you block it.
3 min read
Believemy logo

JavaScript runs one thing at a time. Yet a page animates a banner, listens to five buttons and waits on three requests without ever freezing.

That feat does not come from the language itself, but from the mechanism deciding who goes next: the event loop.


Definition

Compose a program, the loop picks a different order
Add a line:
Your programOutput rank
1console.log("A");→ 1st
2setTimeout(() => console.log("B"), 0);→ 4th
3Promise.resolve().then(() => console.log("C"));→ 3rd
4console.log("D");→ 2nd
Step 1 / 9
Nothing has run yet. The stack is empty, and so are both queues.
Call stack
empty
Microtasks
empty
Tasks
empty
Console
ADCB
Written order: A B C D
Lines out of place: 2 of 4Largest shift: 2 positions

The printed order is not the written order. The script runs to the end first, then the microtask queue drains completely, and only then does one task get its turn. A .then registered after a setTimeout therefore runs before it.

Every setTimeout carries a zero delay, so the tasks are served in registration order; with different delays, the deadline decides between them. The demo shows the callback joining the queue the moment it is registered, whereas the environment only drops it there once the timer has run out: a zero delay really means about a millisecond, which changes nothing here since the script finishes first. Execution time, page rendering and the extra phases of Node.js are left out as well.

Build a program above and compare the order you wrote with the order that runs. That gap explains most asynchronous surprises.

The event loop is the mechanism that constantly checks whether the call stack is empty and, when it is, moves the next waiting task onto it. Three pieces are enough to describe it.

PieceWhat it holds
The call stackThe functions currently running, stacked up
The task queueCallbacks from timers, events and requests
The microtask queuePromise continuations, served first

A setTimeout does not count down inside the engine: it is handed to the environment, browser or Node.js, which drops the callback into the queue once the delay has run out.


One pass through the loop

The rule fits in three beats: run the current code to the end, drain the microtask queue completely, and only then pick up a task.

JAVASCRIPT
console.log("1 synchronous");

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

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

console.log("2 synchronous");

// 1 synchronous
// 2 synchronous
// 3 microtask
// 4 task

A delay of zero milliseconds therefore does not mean right now, it means as soon as the stack is free. A Microtask always goes ahead of it, even when registered later.


Blocking the loop

Since everything shares the same thread, one long function stops everything else from happening: no click is handled, no animation moves, no timer fires.

JAVASCRIPT
const end = Date.now() + 2000;
while (Date.now() < end) {}   // two seconds of frozen page
console.log("the page comes back to life");

Heavy work therefore gets cut into slices that hand control back to the loop between pieces, or moved onto a separate thread. The constraint is the same on the server: under Node.js, one blocking function holds up every request in flight.

Good to know

Node.js follows the same principle with extra phases, among them the timers phase and the input-output phase. Hence setImmediate, which places a callback in the next phase without going through a delay at all.


Frequently asked questions

Question

Does asynchronous code run in parallel?

Not the JavaScript code, no. The waiting genuinely is parallel: a download and a timer countdown are handled by the environment. But the callbacks all come back to the same thread and each takes its turn there.


Question

Why does my 100 ms setTimeout fire later than that?

Because the delay says when the callback becomes eligible, not when it runs. If the stack is busy when the deadline arrives, the callback waits for it to clear. It is a floor, never a guarantee.


Question

How do you move heavy work off the loop?

In a browser, a Web Worker runs the computation on a separate thread and talks back through messages. Under Node.js, the same idea goes through a child process or a worker thread. Those splits shape the performance of a server, a subject worked on in the Node.js course.

Related terms

Discover our javaScript glossary

Every word of JavaScript explained simply: keywords, built-in objects, methods, errors and concepts. Clear definitions and examples that actually run, to learn and to troubleshoot.

Share this article

Want to help us? Share this article on your networks or even better: on your site, in an article or in your newsletter.