JavaScript is Single-Threaded
JavaScript has a single call stack and executes one operation at a time. There is no parallel execution within JS (Web Workers are a separate exception). This means:
- If you run a heavy computation, the browser UI freezes until it finishes
- No two pieces of JS code run simultaneously in the same thread
- The event loop is how JS handles async operations without blocking
The Components of the Event Loop
The Call Stack
The call stack is a LIFO (Last In, First Out) data structure. When a function is called, a stack frame is pushed. When it returns, the frame is popped. JavaScript executes the function on top of the stack.
Never Block the Call Stack
If synchronous code runs for more than ~16ms (one frame at 60fps), the browser cannot repaint — the UI appears frozen. Never run heavy loops, large JSON.parse(), or synchronous file I/O on the main thread. Use Web Workers for CPU-heavy tasks, and async APIs for I/O.
Web APIs — Where Async Work Happens
Web APIs are provided by the browser (or Node.js libuv runtime) and run outside of the JavaScript call stack. When you call setTimeout(fn, 1000), you hand the timer off to the browser. JavaScript moves on immediately. After 1000ms, the browser places fn in the task queue.
This is the key insight: JavaScript never waits. It delegates async work to the runtime environment, registers a callback, and continues executing. When the async work completes, the callback is queued.
Task Queue vs Microtask Queue
| Property | Task Queue (Macrotask) | Microtask Queue |
|---|---|---|
| Sources | setTimeout, setInterval, DOM events, I/O callbacks | Promise .then/.catch/.finally, queueMicrotask, MutationObserver |
| Priority | Lower | Higher — runs FIRST |
| Drain behaviour | One task per event loop tick | ALL microtasks drain before next task |
| Can starve tasks? | No — always gets its turn | Yes — infinite microtask loop blocks tasks |
How the Event Loop Works — Step by Step
- Execute all synchronous code in the call stack until it is empty
- Check the microtask queue — execute ALL microtasks (drain completely)
- If any new microtasks were added during step 2, drain those too
- Pick ONE task from the task queue and execute it (push onto call stack)
- Go back to step 2 — drain microtasks again
- Repeat
async/await and the Event Loop
async/await is syntactic sugar over Promises. Understanding how it maps to the event loop demystifies its behaviour:
Node.js Event Loop Differences
Node.js uses the same event loop concept but with more phases (powered by libuv):
| Phase | What Runs |
|---|---|
| timers | setTimeout and setInterval callbacks (when delay expires) |
| I/O callbacks | Callbacks for I/O operations (file, network) from previous iteration |
| idle, prepare | Internal Node.js use |
| poll | Retrieves new I/O events; executes I/O-related callbacks |
| check | setImmediate() callbacks — runs after poll phase |
| close callbacks | Close event callbacks (e.g. socket.on('close', ...)) |
Node.js also has process.nextTick() — which runs BEFORE the microtask queue (even before Promises). This makes it the highest-priority async mechanism in Node.
Debugging Async Order Issues
When async code runs in an unexpected order, map it to queues: synchronous first, then microtasks (Promises), then tasks (setTimeout). If you need something to run "as soon as current sync code finishes but before I/O", use queueMicrotask() or Promise.resolve().then(). If you need it "after rendering/I/O", use setTimeout(fn, 0).
How We Research and Update This Guide
We test the underlying formula or workflow, compare outputs with reliable references, and revise examples whenever the page content changes.
- The workflow or formula is tested directly in the tool and compared against independent reference examples.
- Examples are kept practical so readers can verify the result without hidden assumptions.
- Pages are revised whenever the interface, calculation flow, or surrounding guidance materially changes.
Frequently Asked Questions — JavaScript Event Loop
JavaScript is single-threaded — it has one call stack and executes one thing at a time. The event loop is the mechanism that allows JS to perform non-blocking operations despite this. When async operations (setTimeout, fetch, DOM events) complete, their callbacks are placed in a task queue. The event loop continuously checks: if the call stack is empty, take the next callback from the queue and push it onto the stack for execution.
The call stack is a data structure that tracks function execution. When a function is called, it is pushed onto the stack. When it returns, it is popped off. JavaScript executes whatever is on top of the stack. If the stack is never empty (e.g. an infinite loop), no callbacks can run — the browser appears frozen. This is why long-running synchronous operations block the UI. Keep synchronous operations fast; delegate heavy work to Web Workers.
The microtask queue (for Promises and queueMicrotask) has higher priority than the task queue (for setTimeout, setInterval, DOM events). After each task, the event loop drains the entire microtask queue before picking the next task. This means Promise callbacks always run before the next setTimeout callback, even if setTimeout(fn, 0). Practical consequence: a deeply-nested Promise chain can delay setTimeout callbacks.
setTimeout with 0ms delay places the callback in the task queue for "as soon as possible after the call stack is empty." But "as soon as possible" means: after the current synchronous code finishes AND after all microtasks (Promises) have drained. So setTimeout(fn, 0) runs after all synchronous code and all pending Promises have settled. It is not truly zero delay.
async/await is syntactic sugar over Promises. An async function runs synchronously until it hits an await — at that point, the function is suspended and the awaited Promise's result is placed in the microtask queue. The event loop then picks up the next task. When the awaited Promise resolves, the continuation of the async function is placed back in the microtask queue. This means async/await does not block the event loop — it pauses the function and lets other code run while waiting.
Web APIs are provided by the browser (or Node.js) — not by JavaScript itself. They include: setTimeout/setInterval, fetch/XMLHttpRequest, DOM events, Geolocation, WebSockets. When you call setTimeout(fn, 1000), the browser's Timer API counts 1000ms outside of JS. When done, it places fn in the task queue. The event loop picks it up when the call stack is empty. JS itself does no waiting — the waiting happens in the browser's C++ runtime.