JavaScript is single-threaded. One call stack, one thing at a time, no two pieces of code running at once. Yet it juggles thousands of concurrent network requests, animations, clicks, and timers without freezing the browser. How?
The event loop is the mechanism behind it. It is not part of the JavaScript language itself. It belongs to the runtime (the browser or Node.js). Understanding it is the difference between async code that behaves and async code that surprises you.
Most developers have a fuzzy mental model. They know setTimeout defers execution and Promises resolve "later." But the order in which "later" happens, and why some code runs before code that was scheduled earlier, stays mysterious. This guide spells out the full model with examples you can run in your browser console.
The Call Stack: One Thing at a Time
The call stack is a data structure that tracks which function is currently executing and which functions called it. When a function is called, it is pushed onto the stack. When it returns, it is popped off.
`javascript
function multiply(a, b) {
return a * b;
}
function square(n) { return multiply(n, n); }
function printSquare(n) { const result = square(n); console.log(result); }
printSquare(4);
`
The call stack at each step:
1. printSquare(4) is pushed
2. square(4) is pushed (called by printSquare)
3. multiply(4, 4) is pushed (called by square)
4. multiply returns 16, popped
5. square returns 16, popped
6. console.log(16) is pushed, executes, popped
7. printSquare returns, popped
8. Stack is empty
If any function takes a long time (a heavy calculation, a synchronous network request), the entire stack is blocked. Nothing else can execute. The browser cannot handle clicks, render animations, or respond to any user input until the stack is clear.
This is why long-running synchronous code freezes the browser. The event loop cannot process new events until the call stack is empty.
Minify your JavaScript for production with the JavaScript Minifier to reduce file size without affecting the execution behavior of your async code.
Web APIs: Where Async Work Happens
When you call setTimeout, fetch, or addEventListener, the JavaScript engine does not handle the timer, network request, or event listening itself. It hands the work off to Web APIs provided by the browser (or C++ APIs in Node.js).
`javascript
console.log('Start');
setTimeout(() => { console.log('Timeout callback'); }, 1000);
console.log('End');
`
Output:
`
Start
End
Timeout callback
`
Here is what happens:
console.log('Start')runs immediately (synchronous)setTimeoutis called. The browser starts a 1-second timer in the Web API layer. ThesetTimeoutcall itself returns immediately and is popped from the stack.console.log('End')runs immediately (synchronous)- The stack is now empty
- After 1 second, the timer completes and the callback is placed in the task queue
- The event loop checks: is the stack empty? Yes. It moves the callback to the stack.
console.log('Timeout callback')runs
The critical insight: the callback does not run after exactly 1 second. It runs after at least 1 second, when the call stack is empty. If the stack is busy with a long-running synchronous operation, the callback waits.
Format your async code consistently with the Code Formatter to make the flow of callbacks, Promises, and async/await easier to follow.

Microtasks vs Macrotasks: The Priority Queue
Not all asynchronous callbacks are treated equally. The event loop distinguishes between macrotasks and microtasks.
Macrotasks (task queue): - setTimeout / setInterval callbacks - I/O operations - UI rendering - MessageChannel - requestAnimationFrame
Microtasks (microtask queue): - Promise .then() / .catch() / .finally() callbacks - MutationObserver callbacks - queueMicrotask()
The event loop processes all microtasks before moving to the next macrotask. After each macrotask completes, the event loop drains the entire microtask queue before doing anything else.
`javascript
console.log('1: script start');
setTimeout(() => { console.log('2: setTimeout'); }, 0);
Promise.resolve().then(() => { console.log('3: Promise'); });
console.log('4: script end');
`
Output:
`
1: script start
4: script end
3: Promise
2: setTimeout
`
Why does the Promise resolve before the setTimeout, even though both are scheduled with zero delay?
- The main script is itself a macrotask. "1: script start" and "4: script end" run synchronously.
- After the main script macrotask completes, the event loop drains the microtask queue. The Promise callback is a microtask. "3: Promise" runs.
- Only then does the event loop pick up the next macrotask. The setTimeout callback runs. "2: setTimeout" appears last.
This priority system is why Promise.resolve().then() always runs before a setTimeout(fn, 0).
Why setTimeout(fn, 0) Is Not Instant
setTimeout(fn, 0) does not mean "run immediately." It means "run as soon as the current task and all microtasks are done, and it is this callback's turn in the task queue."
In practice, setTimeout(fn, 0) has a minimum delay of approximately 4ms in most browsers (the HTML spec allows a minimum of 4ms for nested timeouts beyond 5 levels of nesting). In Node.js, setImmediate(fn) is the equivalent with slightly different timing.
Common uses of setTimeout(fn, 0):
Yielding to the browser: breaking up long-running code to let the browser render and handle user input between chunks.
`javascript
function processLargeArray(items) {
let i = 0;
function chunk() {
const end = Math.min(i + 100, items.length);
while (i < end) {
processItem(items[i]);
i++;
}
if (i < items.length) {
setTimeout(chunk, 0); // yield, then continue
}
}
chunk();
}
`
Deferring DOM updates: ensuring that DOM changes from the current synchronous code are rendered before running additional logic.
Scheduling after microtasks: when you specifically want code to run after all Promises have resolved.
Modern alternatives to setTimeout(fn, 0):
- requestAnimationFrame: runs before the next paint, ideal for visual updates
- requestIdleCallback: runs when the browser is idle, ideal for non-urgent background work
- scheduler.yield() (experimental): explicitly yields to the browser for input handling
- queueMicrotask(): runs after the current task but before the next macrotask
Validate your JSON configurations that control async behavior with the JSON Formatter to ensure they are well-structured.
`setTimeout(fn, 0)` does not mean "run immediately." It means "run as soon as the current task and all microtasks are done, and it is this callback's turn in the task queue." In practice, `setTimeout(fn, 0)` has a minimum delay of approximately 4ms in most browsers (the HTML spec allows a minimum of 4ms for nested timeouts beyond 5 levels of nesting).
Async/Await and the Event Loop
Async/await is syntactic sugar over Promises. Understanding this is essential for predicting execution order.
`javascript
async function foo() {
console.log('A');
await bar();
console.log('B');
}
async function bar() { console.log('C'); }
console.log('D');
foo();
console.log('E');
`
Output:
`
D
A
C
E
B
`
Here is why:
console.log('D')runs synchronouslyfoo()is called. Inside foo,console.log('A')runs synchronously.bar()is called. Inside bar,console.log('C')runs synchronously. bar() returns a resolved Promise.awaitpauses foo(). Everything after the await (console.log('B')) is scheduled as a microtask.- Control returns to the caller.
console.log('E')runs synchronously. - The main script (macrotask) is done. The event loop drains microtasks.
- The continuation of foo() runs.
console.log('B')executes.
The key takeaway: await does not block the thread. It yields control back to the caller and schedules the rest of the function as a microtask. This is why E appears before B even though foo() was called before console.log('E').
Common mistakes with async/await:
- Forgetting that code after
awaitruns later (not immediately) - Using
awaitin a loop whenPromise.allwould be faster (sequential vs parallel) - Not catching errors with try/catch (unhandled Promise rejections)
- Mixing callback-style and async/await style in the same function (hard to follow)

FAQ
Is the event loop part of the JavaScript language specification?
No. The ECMAScript specification defines the language (syntax, types, built-in objects) but not the event loop. The event loop is defined by the HTML Living Standard for browsers and by the libuv library for Node.js. Different environments can implement the event loop differently, which is why Node.js has setImmediate and process.nextTick while browsers do not.
Can microtasks starve macrotasks?
Yes. If microtasks continuously schedule more microtasks, the macrotask queue (and rendering) will be blocked indefinitely. For example, a recursive Promise.resolve().then(recurse) will freeze the browser because the event loop never finishes draining the microtask queue. Always ensure microtask chains terminate.
How does requestAnimationFrame fit into the event loop?
requestAnimationFrame callbacks run before the browser paints each frame (roughly every 16.7ms at 60fps). They are not macrotasks or microtasks. They occupy their own phase in the event loop, between microtask draining and rendering. This makes them ideal for visual updates because they are synchronized with the display refresh rate.
Does Web Workers change how the event loop works?
Web Workers have their own event loop running on a separate thread. They do not share the main thread's call stack or event loop. Communication between the main thread and a Worker happens through message passing (postMessage), which adds messages to each thread's respective task queue. Workers solve the problem of CPU-intensive tasks blocking the main thread.
### Is the event loop part of the JavaScript language specification.
Markdown Table Generator: Build Clean Tables Without the Pain
Markdown tables are simple until the pipes and dashes stop lining up. Learn the syntax, alignment tricks, and a free tool that formats tables for you.
CSV to JSON: Convert Spreadsheet Data for APIs and Code
Turn a CSV export into clean JSON for APIs, imports, and scripts. Learn how the conversion works, common pitfalls with types and quotes, and a free tool.
JSON Guide: Format, Validate, and Convert JSON Files
JSON guide for developers: syntax rules, common parse errors, formatting and schema validation, plus how to convert between JSON and CSV files.
