Event Loop
The mechanism that lets JavaScript handle many tasks without ever running two at once.
What is it?
JavaScript can only do one thing at a time — it has a single "thread" of execution. And yet it can handle things like timers, network requests, and user clicks all seemingly "at once" without freezing the page. The event loop is the mechanism that makes this possible.
It works by separating "run this code right now" from "run this code later, once something else finishes" — and constantly checking whether it's time to run any of that waiting code.
Explain like I'm 10
Imagine a single chef in a kitchen who can only cook one dish at a time, but has an oven timer for things in the background. The chef finishes the current dish, checks if any timers have gone off, handles those, then moves to the next task — never doing two things simultaneously, but never sitting idle either.
Examples
Order of execution
console.log("1: start");
setTimeout(() => {
console.log("2: timeout callback");
}, 0);
console.log("3: end");
// Output order:
// 1: start
// 3: end
// 2: timeout callbackEven with a 0ms delay, the timeout callback runs after all the regular code — because it has to wait for the main code to finish first.
Promises run before timers
console.log("1: start");
setTimeout(() => console.log("2: setTimeout"), 0);
Promise.resolve().then(() => console.log("3: promise"));
console.log("4: end");
// Output order: 1, 4, 3, 2Promise callbacks (microtasks) are always drained before the next timer callback (a macrotask) runs, even if both were scheduled at roughly the same time.
How it works
JavaScript runs your main code on something called the call stack. When it encounters something asynchronous (like setTimeout or a network request), that task is handed off to the browser, and JavaScript keeps running the rest of the main code. Once the async task finishes, its callback is placed in a queue — but there are actually two of them, checked in a strict order. Promise callbacks go into the microtask queue; timers, network events, and clicks go into the macrotask queue. The event loop's job is: once the call stack is empty, drain the entire microtask queue first — running every waiting promise callback, even ones added while draining — and only once it's completely empty does it run a single macrotask, before checking the microtask queue again.
Call stack (running now)
↓ empty?
Event loop checks the queue
↓
Queue has a waiting callback? ──▶ run it on the call stack
↓ no
keep checkingWhy does it exist?
Without the event loop, any slow task (like a network request) would freeze the entire page until it finished. The event loop lets JavaScript start slow tasks, move on immediately, and come back to handle the result later — keeping the page responsive the whole time.
When to use it
You reach for this mental model any time you're debugging unexpected ordering — why a console.log ran before a network response, why a setTimeout(fn, 0) didn't run immediately, or why the page froze during a long calculation.
When not to use it
Day-to-day, you don't manage the event loop directly — there's no API to configure it. It's not something you "use"; it's something you understand so that async code (promises, timers, events) makes sense instead of feeling random.
Common mistakes
Assuming
setTimeout(fn, 0)runs immediately — it still waits for the current code to finish first.Not realizing that a long-running synchronous loop can freeze the page, since nothing else can run until the call stack is clear.
Confusing the order of promise callbacks (microtasks) and timer callbacks (macrotasks) — microtasks always finish draining before the next macrotask runs, it's not just a usual tendency.
Practice exercises
- Easy:
Predict, then verify, the console output order of a mix of
console.logandsetTimeoutcalls. - Medium:
Write a small snippet mixing a
Promise.resolve().then()and asetTimeout, and explain which runs first and why. - Hard:
Explain, in your own words, why a
forloop with a billion iterations would freeze a webpage.
Interview questions
Is JavaScript single-threaded?
Yes — it runs one line of code at a time on a single call stack, but the browser or Node.js provides separate mechanisms (timers, network APIs, file I/O) that do slow work in the background and only hand results back to that single thread through the event loop.
What is the difference between the call stack and the task queue(s)?
The call stack is where code is actually executing right now, one function frame at a time. The task queues just hold callbacks — from timers, promises, DOM events — that are waiting for the stack to be completely empty before the event loop is allowed to run any of them.
Do microtasks (promises) or macrotasks (`setTimeout`) run first?
Microtasks run first — every time the call stack empties, the event loop drains the entire microtask queue, including any new microtasks scheduled while draining, before it's allowed to run even a single macrotask.
What concretely counts as a macrotask versus a microtask?
Macrotasks include setTimeout/setInterval callbacks, DOM events (clicks, keypresses), and network callbacks — each 'task' from the host environment. Microtasks include promise .then()/.catch()/.finally() callbacks and queueMicrotask() callbacks — jobs scheduled by the JavaScript engine itself, which are always given priority over the next macrotask.
Where does `queueMicrotask()` fit relative to promise callbacks?
It schedules a callback into the exact same microtask queue that promise callbacks use, so it follows the identical ordering rule: it runs after the current synchronous code finishes, and before the next macrotask, in the order it was queued relative to other microtasks.
Predict the output: `console.log(1); setTimeout(() => console.log(2), 0); Promise.resolve().then(() => console.log(3)); console.log(4);`
1, 4, 3, 2. The two synchronous logs run first. Then, once the stack is empty, the entire microtask queue drains, so the promise's 3 runs next. Only after that, with the microtask queue empty, does the event loop pick up the timer's macrotask and log 2.
What does it actually mean that 'the call stack must be empty' before the event loop can act?
It means the event loop never interrupts running JavaScript mid-function to run a queued callback — it only checks the queues at the exact moment every function on the stack has returned. This is why a long synchronous operation delays every pending timer, promise callback, and UI event, no matter how long they've been waiting.
Why doesn't `setTimeout(fn, 0)` run its callback immediately?
Because a timer callback is a macrotask, and macrotasks always have to wait for the current synchronous code to finish and for the entire microtask queue to drain first — 0 just means 'as soon as possible after that,' not 'right now,' and browsers additionally clamp very short or deeply nested timeouts to a small minimum delay.
Does the event loop run through the entire macrotask queue in one go, the way it drains the whole microtask queue?
No — it takes exactly one macrotask per pass, then drains the microtask queue completely again before taking the next single macrotask. This alternation (one macrotask, then all microtasks, repeat) is different from how the microtask queue itself is always fully emptied each time.
Predict the output: two `setTimeout(fn, 0)` calls are scheduled, and inside the first timer's callback a `Promise.resolve().then()` is scheduled. Does that promise callback run before or after the second timer's callback?
It runs before the second timer's callback. Once the first timer (a macrotask) finishes running, the event loop drains the microtask queue completely — including the promise callback just scheduled inside it — before it's allowed to move on to the second timer, even though the second timer was scheduled earlier.
If a microtask callback schedules another microtask, does the event loop wait for that one too before moving to the next macrotask?
Yes — the rule is 'drain the microtask queue until it's completely empty,' not 'run whatever was in it at the start.' Any microtask added while draining still gets processed in the same pass, before a single macrotask is allowed to run.
Why can a chain of promises that keeps scheduling new microtasks prevent timers from ever firing, a phenomenon sometimes called 'microtask starvation'?
Because the event loop only ever moves to a macrotask once the microtask queue is completely empty, and a promise chain that keeps re-scheduling itself (each .then() queuing another) never lets that queue empty out — so pending setTimeout callbacks, UI events, and network callbacks all get starved indefinitely, even though nothing is technically 'blocking' in the synchronous sense.
Where do browser rendering and repaints happen relative to microtasks and macrotasks?
A browser generally tries to repaint after the microtask queue has drained and before running the next macrotask (though it doesn't have to repaint on every single cycle) — which is why microtasks that never finish accumulating can also visibly freeze the page's rendering, not just its logic.
What is `requestAnimationFrame`, and how does its timing differ from a macrotask like `setTimeout`?
It schedules a callback to run right before the browser's next repaint, synced to the display's refresh rate, rather than after an arbitrary delay like setTimeout. It's specifically meant for visual updates, so the browser can batch and time it correctly, unlike a generic timer which has no relationship to the paint cycle.
Predict the output: a script logs 'start', calls `queueMicrotask` to log 'micro', calls `setTimeout(fn, 0)` to log 'macro', then logs 'end'.
'start', 'end', 'micro', 'macro'. The two synchronous logs run first; once the stack empties, the microtask queue is drained (logging 'micro'), and only then does the event loop pick up the timer macrotask and log 'macro'.
What does 'blocking the event loop' mean, concretely?
It means running synchronous JavaScript that takes a long time to finish, during which the call stack is never empty — so no timers can fire, no promise callbacks can run, and the page can't respond to clicks, scrolls, or repaints, because the single thread is entirely occupied by that one piece of code.
Why can one very expensive synchronous computation freeze an entire web page, including things seemingly unrelated to it like scrolling?
Scrolling, click handling, and rendering all rely on the same single thread being free to process their queued callbacks. As long as a synchronous loop keeps the call stack occupied, none of those queued items — regardless of what feature they belong to — get a chance to run, because the event loop can't interrupt already-running code.
Do Web Workers get around JavaScript's single-threaded limitation, and how do they relate to the main thread's event loop?
Yes — a Web Worker runs on a genuinely separate thread with its own call stack and its own event loop, so heavy computation there doesn't block the main thread. It communicates back to the main thread only via message passing (postMessage), which itself arrives as a macrotask-like event on the main thread's queue, rather than sharing memory and state directly.
Conceptually, how does Node.js's event loop differ from a browser's?
Both share the same core idea — one thread, a call stack, and callback queues drained once the stack is empty — but Node.js organizes its macrotasks into distinct phases (timers, I/O callbacks, setImmediate, close callbacks, and so on) that run in a fixed order each loop iteration, whereas a browser's model is comparatively simpler and doesn't expose those phases directly.
Predict the output: an `async` function awaits an already-resolved value and logs after the `await`; a `setTimeout(fn, 0)` is scheduled right before calling that async function; a synchronous log happens right after calling it.
The synchronous log after the call runs first, then the async function's post-await log, then the timer's log last. Resuming after an await is scheduled as a microtask, so it's still prioritized ahead of the macrotask queue that the timer's callback sits in, even though the timer was scheduled earlier in the source.
Why does resuming after an `await` behave, timing-wise, just like a promise `.then()` callback?
Because that's exactly what it is under the hood — await desugars to registering a continuation on a promise, and that continuation is scheduled into the microtask queue the same way any .then() callback would be, which is why async/await follows the identical microtask-before-macrotask ordering rules.
Predict the output: `console.log(1); Promise.resolve().then(() => console.log(2)); Promise.resolve().then(() => console.log(3)).then(() => console.log(4)); console.log(5);`
1, 5, 2, 3, 4. Both synchronous logs run first. Then the microtask queue is processed in the order things were scheduled: 2 was queued first and runs; 3 was queued right alongside it and runs next; 3's own .then() for 4 is only scheduled once 3 finishes running, so 4 runs last, after 2 and 3.
A `setTimeout` callback with a 100ms delay ends up running noticeably later than 100ms after it was scheduled. What event-loop-related reasons could explain that?
The timer only becomes eligible to run after 100ms — it still has to wait for the call stack to be empty and, on each pass, for the entire microtask queue to drain first. If other synchronous code is still running, or a long-running or continuously-refilled microtask queue is monopolizing every gap, the timer callback can be delayed well beyond its nominal delay.
Why is it more accurate to think of `setTimeout`'s delay as a minimum rather than a guarantee?
The delay only guarantees the callback won't run before that time has elapsed — it says nothing about how soon after that it will actually execute, since it still has to wait its turn behind the currently running code, the microtask queue, and any macrotasks ahead of it in the queue.
Why doesn't calling `Promise.all()` to wait on several promises block the event loop while they're pending?
Promise.all() doesn't loop or poll waiting for its inputs — it just registers callbacks on each promise and returns immediately, letting the call stack clear. The actual waiting happens passively; nothing runs again until one of the underlying promises settles and its callback is scheduled, so the thread is free to do other work in the meantime.
Mechanically, why is a UI click handler just another item competing for the same queue as a `setTimeout` callback?
A click is detected by the browser outside of JavaScript, and instead of interrupting whatever's currently running, it queues the registered handler as a macrotask, exactly like a timer firing — so a page busy running a long synchronous script won't respond to a click until that script finishes and the event loop gets around to that queued task.
If a piece of code schedules two `setTimeout(fn, 0)` calls back to back, does the one scheduled first always fire before the second?
Generally yes, since timers of equal delay fire in the order they became eligible — but it's not an absolute guarantee: browsers clamp very short or deeply nested timeout chains to a small minimum delay (historically around 4ms), and other queued work in between can affect exact ordering in edge cases.
What's the difference, in terms of end effect versus root cause, between starving the main thread with an infinite chain of microtasks versus with one long synchronous loop?
Both freeze the page identically from the outside — nothing else gets a turn to run. The difference is in the mechanism: a synchronous loop keeps the call stack itself continuously busy, while a runaway microtask chain repeatedly empties and refills the microtask queue, so the call stack does briefly empty between each one, but the event loop is never allowed to reach the next macrotask because that queue is never truly finished.
Why is JavaScript's split into 'run this now' and 'run this later, once something else finishes' the whole reason the event loop needs to exist at all?
Because JavaScript only has one thread, it has no way to genuinely wait for a slow operation without freezing everything else. Splitting work into synchronous code plus deferred callbacks lets that single thread start slow operations, immediately move on to other work, and come back to handle results later — the event loop is simply the mechanism that manages when 'later' actually arrives.
What is the difference between a task and a microtask job in loose spec terms, and why does the distinction matter in practice?
A 'task' (macrotask) is host-defined work like a timer firing or a network event arriving; a 'microtask' (often called a 'job' in the spec, largely driven by promises) is engine-scheduled follow-up work. The distinction matters because the engine always fully drains all pending jobs before yielding to the host to run the next task, which is the entire reason promise callbacks consistently beat timer callbacks of the same nominal delay.