DEV Community

Abhishek Mishra
Abhishek Mishra

Posted on

Event Loop Deep Dive: What Every Frontend Dev Should Know — Timers, Microtasks, and Real Examples

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

  1. The big picture — what the event loop actually is
  2. Tasks (macrotasks) vs microtasks — the crucial difference
  3. Where timers, rAF and promises fit
  4. Concrete examples — paste these into your browser console
  5. Real world gotchas & best practices
  6. Debugging tips & tools
  7. 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 after await.
  • 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');
Enter fullscreen mode Exit fullscreen mode

Expected output:

script start
script end
promise.then (microtask)
promise.then 2 (microtask)
setTimeout (macrotask)
Enter fullscreen mode Exit fullscreen mode

✅ 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');
Enter fullscreen mode Exit fullscreen mode

Expected output:

start
inside async
end
after await (microtask)
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

Typical output:

begin
finish
promise (microtask)
rAF (animation frame)
timeout (macrotask)
Enter fullscreen mode Exit fullscreen mode

5. Real world gotchas & best practices

🧠 Long loops freeze your UI

If you run:

for (let i = 0; i < 1e9; i++) {}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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)