What is the JavaScript Event Loop?
The event loop is a mechanism provided by the JavaScript environment (like your browser or Node.js) that enables asynchronous programming. It manages how and when different tasks—such as timers, promises, and event callbacks—get executed, making sure that everything happens in the right order, even though JavaScript itself can only do one thing at a time.
Imagine you walk into a kitchen. There’s a chef, focused and busy. He’s great at his job, but here’s his one weird rule: he can only do one thing at a time. If he’s stirring a pot, he can’t chop onions. If he’s answering the phone, he can’t check the oven. This chef is JavaScript (single-threaded), running step by step.
But, of course, a kitchen isn’t just a chef. There are timers on the oven, deliveries to track, and maybe a robot that watches for when the fridge is left open. All these “background jobs” are the Web APIs, not part of the chef’s mind, but part of the kitchen he works in.
Now, here’s the crucial part:
The event loop, the trays where jobs wait, and the Web APIs are all provided by the kitchen itself—the environment.
They’re not part of the chef’s brain (the JavaScript engine, like V8 or SpiderMonkey). This means that when you write JavaScript, the chef can only follow instructions; he needs the kitchen (the browser, Node.js, etc.) to do all the real-world, slow, or event-based jobs.
How do the Chef and Kitchen communicate? (The Real Story)
Let’s make this concrete. When the chef encounters a task he can’t do himself—like setting a timer or making an HTTP request—he turns to his helpers. Here’s how the whole process actually unfolds:
Chef hits an async task:
Suppose your code sayssetTimeout
. The chef (JS engine) can’t actually wait or time things. So, he asks the kitchen’s timer (Web API): “Please handle this, and let me know when time’s up.”The helper (Web API) takes over:
The kitchen’s timer starts counting, completely independently from the chef, who just moves on. The chef doesn’t care about the timer anymore—he has other work to do.When the timer is done:
The kitchen’s timer writes a note: “This task is ready!” But the kitchen can’t just barge in and interrupt the chef; that would mess up the recipe! So the kitchen puts the note on the callback queue (the tray).The event loop keeps watch:
There’s an invisible manager (the event loop) who keeps checking:
- “Is the chef busy with the main recipe (call stack)?”
- “If not, is there a note on any tray (macrotask or microtask queue)?” If yes, the manager gives the next task from the tray to the chef.
- The chef never talks directly to the helpers again: The communication always goes through the trays and the event loop. The chef (JavaScript engine) and the kitchen (environment) are separate; they only “talk” via these shared trays.
The same thing happens for things like HTTP requests, file reading, DOM events, or geolocation—the chef delegates to the kitchen’s specialized helper (Web API), and when they’re done, the callback goes on the tray, waiting its turn.
Code Analogy: The communication in action
Let’s see it step by step:
console.log('Step 1: Start Cooking');
// Chef asks the kitchen to start a timer
setTimeout(() => {
console.log('Step 5: Cake is ready!');
}, 1000);
// Chef asks the kitchen to fetch groceries
fetch('https://some.api/ingredients')
.then(() => console.log('Step 6: Groceries arrived!'));
console.log('Step 2: Chopping veggies');
// Chef queues a promise microtask
Promise.resolve().then(() => {
console.log('Step 4: Finished a quick microtask!');
});
console.log('Step 3: Making sauce');
What really happens?
- The chef prints Step 1.
- He asks the kitchen to start a timer (
setTimeout
). The timer runs in the kitchen (Web API land). - He asks the kitchen to fetch groceries. The fetch starts in the kitchen, out of sight for the chef.
- He sees a Promise. That goes on a special “microtask” tray.
- He prints Step 2 and Step 3, continuing his work.
- Now, he’s done with the immediate recipe. The event loop checks the microtask tray first. The Promise callback is there, so he handles it: prints Step 4.
- Then, the chef looks to the regular tray. Maybe the timer is done (after 1s), so Step 5 gets printed.
- When the groceries arrive, their callback (from fetch) is also placed on the regular tray (macrotask queue), so Step 6 prints after everything else that’s already waiting.
Order of output:
Step 1: Start Cooking
Step 2: Chopping veggies
Step 3: Making sauce
Step 4: Finished a quick microtask!
Step 5: Cake is ready! // after 1 second
Step 6: Groceries arrived! // when the fetch completes
Notice how the chef (JS engine) and the kitchen helpers (Web APIs) coordinate everything without ever stepping on each other’s toes, thanks to the trays and the event loop.
A Peek behind the kitchen door: Microtasks vs Macrotasks
You might wonder: Why are there two trays?
Well, in JavaScript, microtasks (promises, mutation observers, queueMicrotask) have higher priority. After every batch of main recipe steps, the chef always clears the microtask tray before he looks at the macrotask tray (timers, fetch, events). This guarantees that microtasks like resolved promises happen as soon as possible, before any new timers or events.
Common Gotchas: Where most developers get surprised
setTimeout(..., 0) doesn’t run immediately!
Even with a zero delay, setTimeout callbacks are always placed in the macrotask queue, so all microtasks (like Promise callbacks) will run first.Promise callbacks (“microtasks”) always run before timers (“macrotasks”).
If you have both in your code, the promise’s.then()
will execute before setTimeout, every time.Long chains of microtasks can block everything else.
If you keep queuing microtasks inside other microtasks, the event loop can get “stuck” processing them, delaying other tasks like timers or UI updates.Async code doesn’t mean “in parallel.”
JavaScript is single-threaded. “Async” just means “wait your turn”—not “run at the same time.”Event callbacks (like click or keydown) also wait their turn!
UI events are queued just like any other macrotask—they don’t interrupt code that’s already running.
What about events like clicks or keyboard input?
That’s the kitchen’s job too!
When you click a button, the kitchen sees it and, when the chef is free, puts a note on the macrotask tray. The event loop delivers it to the chef, who then runs your click handler code.
And If Things Get Crazy? (Infinite Microtasks, Starvation, etc)
Here’s a secret:
If you keep adding microtasks inside other microtasks, the chef could get stuck on the VIP tray forever, and never move on to macrotasks!
That’s called starvation. JavaScript tries to avoid it, but it’s up to you as a developer to be careful—don’t flood the microtask queue with endless callbacks.
Advanced Extras: Animation Frames, Node.js, and more complex examples
What about requestAnimationFrame?
Besides microtasks and macrotasks, browsers have a special queue for animation callbacks: the animation frame queue.
When you call requestAnimationFrame(callback)
, the callback runs before the next browser repaint—after all microtasks and regular tasks for the frame are finished.
This is perfect for smooth animations, because the browser chooses the best moment for your code to update the UI.
Analogy:
The chef (JS engine) has finished his trays, but before starting new work, the kitchen manager (event loop) checks, "Anything for the animation?" If so, it gets handled right before serving the next meal.
console.log('A');
requestAnimationFrame(() => console.log('B'));
Promise.resolve().then(() => console.log('C'));
console.log('D');
Output order:
A
D
C
B
What about Node.js? (Event Loop Phases)
Node.js runs JavaScript outside the browser, and its event loop is split into phases:
-
Timers: Executes
setTimeout
andsetInterval
callbacks. - Pending Callbacks: I/O callbacks deferred to the next loop.
- Poll: Retrieves new I/O events and executes their callbacks.
-
Check: Executes
setImmediate
callbacks. - Close Callbacks: For closed connections or resources.
Promise microtasks always run between these phases, just like in the browser.
If you’re digging into Node, check the official Node.js guide.
Complex Code Execution: Step-by-step Examples
Example 1: Chained Promises vs setTimeout
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => {
console.log('3');
Promise.resolve().then(() => console.log('4'));
});
console.log('5');
Order:
1
5
3
4
2
Example 2: setTimeout inside a Promise
console.log('a');
Promise.resolve().then(() => {
console.log('b');
setTimeout(() => console.log('c'), 0);
});
setTimeout(() => console.log('d'), 0);
console.log('e');
Order:
a
e
b
d
c
Example 3: Starvation with Endless Microtasks
let count = 0;
function recur() {
if (count < 5) { // Try count < 1_000_000 for real starvation!
count++;
Promise.resolve().then(recur);
}
}
recur();
setTimeout(() => console.log('Timer fired!'), 0);
If you remove the limit, the timer may never fire—the microtask queue "starves" the timer!
Example 4: Animation, Microtasks, and Timers Together
console.log('start');
requestAnimationFrame(() => console.log('anim'));
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('end');
Order:
start
end
promise
timeout
anim
Tip:
When in doubt, remember the order:
- Synchronous code (call stack)
- Microtasks (promises)
- Animation frame queue (in browser)
- Macrotasks (timers, events)
- Repeat!
Conclusion: What should you take away?
The JavaScript event loop is what makes single-threaded JavaScript feel so much more powerful—like a chef that always keeps the kitchen running smoothly, no matter how many jobs are waiting.
So next time you see async code, timers, or promises, remember:
- The chef (JavaScript) never gets overwhelmed, because the kitchen (the environment) and the event loop keep things organized.
- If something doesn’t run when you expect, check which tray it’s waiting on!
Want to see this in action? Try out your own code examples in the browser console, or grab one of the above and play with the order. The best way to master the event loop is to experiment.
If you have questions, favorite “gotchas,” or other analogies that helped you, share them in the comments below!
Let’s help more developers see that behind every callback and promise, there’s just a chef and a kitchen doing their best work—one task at a time.