If you’ve ever wondered why Promise.resolve().then(...)
runs before setTimeout(..., 0)
, or why your UI freezes while a loop “just counts to a million,” this post is for you.
Let’s break down the JavaScript event loop — in simple terms — with real examples you can run right in your browser console.
Table of contents
- The big picture — what the event loop actually is
- Tasks (macrotasks) vs microtasks — the crucial difference
- Where timers, rAF and promises fit
- Concrete examples — paste these into your browser console
- Real world gotchas & best practices
- Debugging tips & tools
- Wrap-up / TL;DR
1. The big picture — what the event loop actually is
Think of JavaScript as a single worker in your system — one person doing one task at a time.
No extra threads, no multitasking magic.
Here’s how it works:
- The call stack is what the worker is doing right now.
- The event loop is like the manager who keeps checking, “Is the worker done? What’s next on the list?”
- The task queues are the list of pending jobs waiting for their turn.
There are two main kinds of tasks waiting to be picked:
-
Macrotasks — bigger jobs like
setTimeout
, I/O, user events. -
Microtasks — smaller, “finish this right after the current thing” jobs, like
Promise.then
.
👉 After one macrotask finishes, the event loop runs all the microtasks before starting the next macrotask.
That’s the golden rule. If you understand that, 80% of event loop mysteries disappear.
2. Tasks (macrotasks) vs microtasks — the crucial difference
Macrotasks:
- Examples:
setTimeout
,setInterval
, DOM events, I/O. - These are scheduled to run later, after microtasks are done.
Microtasks:
- Examples:
Promise.then
,queueMicrotask
,async/await
afterawait
. - These run right after the current code finishes, before rendering or the next macrotask.
Think of it like this:
- Macrotask = “Do it later.”
- Microtask = “Do it immediately after this.”
3. Where timers, rAF and promises fit
Function | Type | When it runs |
---|---|---|
setTimeout(fn, 0) |
Macrotask | After all microtasks finish |
Promise.then(fn) |
Microtask | Right after current code finishes |
queueMicrotask(fn) |
Microtask | Same as Promise.then timing |
requestAnimationFrame(fn) |
Special | Before the next screen paint |
4. Concrete examples — paste these into your browser console
Example 1 — Basic ordering
console.log('script start');
setTimeout(() => console.log('setTimeout (macrotask)'), 0);
Promise.resolve()
.then(() => console.log('promise.then (microtask)'))
.then(() => console.log('promise.then 2 (microtask)'));
console.log('script end');
Expected output:
script start
script end
promise.then (microtask)
promise.then 2 (microtask)
setTimeout (macrotask)
✅ Microtasks run before macrotasks. Always.
Example 2 — async/await under the hood
console.log('start');
async function foo() {
console.log('inside async');
await null;
console.log('after await (microtask)');
}
foo();
console.log('end');
Expected output:
start
inside async
end
after await (microtask)
await
always pauses the function and resumes it as a microtask later.
Example 3 — requestAnimationFrame vs others
console.log('begin');
requestAnimationFrame(() => console.log('rAF (animation frame)'));
setTimeout(() => console.log('timeout (macrotask)'), 0);
Promise.resolve().then(() => console.log('promise (microtask)'));
console.log('finish');
Typical output:
begin
finish
promise (microtask)
rAF (animation frame)
timeout (macrotask)
5. Real world gotchas & best practices
🧠 Long loops freeze your UI
If you run:
for (let i = 0; i < 1e9; i++) {}
your browser tab becomes unresponsive — the event loop can’t do anything else.
✅ Split heavy tasks:
function doWorkInChunks() {
for (let i = 0; i < 100000; i++) { /* work */ }
if (stillWorkLeft) setTimeout(doWorkInChunks, 0);
}
⚡ setTimeout(..., 0)
is not instant
Even with 0
, the browser decides when to actually run it — it might be delayed.
For “run this right after current code,” use queueMicrotask()
or Promise.resolve().then()
.
🔁 Microtask loops can block rendering
If you keep queueing microtasks inside microtasks, you can stop the browser from ever updating the screen.
Don’t overdo it — use a timer or rAF
to give the UI a break.
6. Debugging tips & tools
- Chrome DevTools → Performance tab: record and check long tasks.
- Performance Monitor: view FPS, JS memory, and CPU usage.
- Lighthouse audits: detect scripts that block main thread.
- Try the console experiments above — it’s the fastest way to really “get it”.
7. Wrap-up / TL;DR
-
Microtasks (Promises,
queueMicrotask
) run before macrotasks (setTimeout
). - requestAnimationFrame runs before the browser’s next repaint.
- Don’t block the event loop — chunk your heavy work or move it to a web worker.
- When debugging “weird async timing,” think: current code → microtasks → render → next macrotask.
Top comments (0)