// js
What Is the Event Loop in JavaScript? Async Explained
JavaScript runs on one thread, yet handles timers, network and clicks without freezing. The event loop is how: the call stack, the task and microtask queues, and why promises run before setTimeout.
JavaScript has a famous quirk: it runs your code on a single thread, doing one thing at a time — and yet it can wait for a network request, run a timer, and respond to clicks without ever freezing. The thing that makes that possible is the event loop. Understanding it is what turns asynchronous JavaScript from confusing to obvious.
One thread, one call stack
JavaScript executes code on a single call stack: functions are pushed on when called and popped off when they return, one at a time. Because there is only one stack, only one piece of code runs at any moment. If a function takes a long time, nothing else — not even a button click — can run until it finishes. That is why a heavy loop "freezes" the page.
So how does JavaScript wait for a 2-second timer or a slow download without blocking everything? The trick is that it does not wait at all. It hands those jobs to the surrounding environment and keeps going.
The environment does the waiting
The browser (and Node.js) gives JavaScript extra powers that are not part of the language itself: timers, network requests, file access, event listeners. When you call setTimeout or fetch, the engine hands that task to the environment and immediately moves on. The environment does the waiting in the background, and when the job is done it places your callback into a queue — not back onto the stack directly.
What the event loop actually does
This is where the event loop comes in. Its job is simple to state: whenever the call stack is empty, take the next callback from the queue and push it onto the stack to run. It loops forever, checking "is the stack empty? is there anything waiting?" and feeding queued callbacks in one at a time. That single rule is the whole mechanism behind asynchronous JavaScript.
The key consequence: your queued callbacks only run after all the current synchronous code has finished. A setTimeout(fn, 0) does not run "now" — it runs once the stack is clear, even if that is later than you expect.
Microtasks vs macrotasks: why promises win
There is not one queue but (at least) two, and the difference explains the most common interview gotcha. Macrotasks include things like setTimeout and I/O callbacks. Microtasks include resolved promise callbacks (.then) and queueMicrotask. The rule is that after each macrotask, the event loop drains the entire microtask queue before moving on.
That is why this prints start, end, promise, timeout — the promise runs before the timer even though the timer was set to 0:
console.log('start');
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('end'); The two console.log calls are synchronous and run first. The promise callback is a microtask and runs as soon as the stack clears. The timeout is a macrotask and waits its turn after the microtasks.
Why this matters in practice
The event loop explains the behaviour that trips people up: why setTimeout(fn, 0) is "as soon as possible" rather than "immediately", why a long synchronous calculation makes the whole page unresponsive, and why async/await code resumes only after the surrounding synchronous work is done. Once you picture the stack emptying and the loop feeding in queued callbacks, asynchronous JavaScript stops being mysterious.
Node.js vs the browser
The concept is the same in Node.js, but the host is different: instead of browser Web APIs, Node uses a library called libuv to handle timers, file system and network work off the main thread. Node's loop has a few extra phases (and process.nextTick sits even ahead of normal microtasks), but the mental model is identical — one thread, an environment doing the waiting, and a loop feeding finished work back in.
The bottom line
The event loop is how single-threaded JavaScript stays responsive: synchronous code runs on the call stack, slow work is handed to the environment, and finished callbacks wait in queues until the stack is empty. Microtasks (promises) run before macrotasks (timers), and nothing async ever interrupts code that is already running. Hold that picture and the rest of async JavaScript — promises, async/await, timers — falls into place.
Frequently asked questions
- What is the event loop in JavaScript?
- It's the mechanism that lets single-threaded JavaScript stay responsive: whenever the call stack is empty, the event loop takes the next queued callback and runs it. Slow work such as timers and network requests is handed to the environment, which queues your callback once it finishes.
- Why does a promise run before setTimeout?
- Promise callbacks are microtasks and timers are macrotasks. After each macrotask the event loop drains the entire microtask queue first, so a resolved promise’s .then runs before a setTimeout(…, 0) callback even when the timer was set to zero.
- Does setTimeout(fn, 0) run immediately?
- No. It runs as soon as the call stack is clear and after any pending microtasks, not instantly. "0" means "as soon as possible", which is still after the current synchronous code finishes.
- Is the event loop the same in Node.js and the browser?
- The mental model is the same — one thread, the environment does the waiting, and a loop feeds finished callbacks back in. Node.js uses libuv and has a few extra phases (and process.nextTick runs ahead of normal microtasks), but the core idea is identical.