Neural DownloadNEURAL DOWNLOAD
← cd ../blog

How JavaScript's Event Loop Actually Works

How JavaScript runs 1000 async tasks on a single thread. The call stack, task queue, microtask queue, and Web APIs — visualized.

javascript event loopevent loop explainedjavascript asyncmicrotasks vs macrotasksjavascript call stackpromise vs settimeout
Share

What order do these three lines print?

console.log("first");
setTimeout(() => console.log("second"), 0);
Promise.resolve().then(() => console.log("third"));

Most people guess first, second, third. The actual output is first, third, second. The zero-delay timeout prints *after* the promise. By the end of this article, you'll know exactly why.

One Thread, One Stack

JavaScript has one thread. One. Every function call goes on the call stack. When it returns, it comes off. If a function takes five seconds, everything stops — clicks don't register, scrolling dies, animations hang. The UI thread is frozen.

So how does JavaScript handle thousands of concurrent operations? It doesn't. Not alone.

The Runtime Gives It Superpowers

JavaScript the language is single-threaded, but it runs inside a runtime — and the runtime provides Web APIs: timers, network requests, DOM events. These aren't part of JavaScript itself.

When you call setTimeout, JavaScript hands the timer to the browser and moves on. The call stack is free. When the timer fires, the callback enters a waiting room: the task queue.

The event loop watches both the call stack and the task queue. When the stack is empty, it grabs the next callback from the task queue and pushes it onto the stack.

This is why setTimeout(fn, 0) doesn't mean "run now." It means "run as soon as the stack is empty." If the stack is busy, that zero-millisecond callback waits.

The VIP Queue

Now back to our puzzle. Why did the promise callback jump ahead of the timeout?

Because promises don't use the task queue. They use a separate microtask queue — and it has VIP access.

QueuePriorityExamples
MicrotaskDrains completely after every taskPromise.then, MutationObserver, queueMicrotask
Task (macro)One per loop iterationsetTimeout, setInterval, click handlers, network callbacks

After each task finishes, the event loop drains the entire microtask queue before touching the next task. Every microtask runs first — and if a microtask schedules another microtask, that runs too.

So in our puzzle: "first" prints synchronously. setTimeout registers in the task queue. Promise.resolve().then schedules in the microtask queue. Script finishes. Microtasks drain: "third". Then the task queue: "second".

And async/await? Same mechanism underneath. When you write await, you're writing a Promise.then. The continuation gets scheduled as a microtask.

The Complete Cycle

Every iteration of the event loop follows three steps:

  1. Task — pick one callback from the task queue, run it
  2. Microtasks — drain the entire microtask queue
  3. Render — if it's time (~60fps), run requestAnimationFrame, recalculate styles, reflow, paint

Then back to step one. Forever.

Test yourself:

console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
requestAnimationFrame(() => console.log("D"));
Promise.resolve().then(() => console.log("E"));
console.log("F");

The answer: A, F, C, E, B, D. Synchronous first, microtasks second, then tasks and render callbacks.

Every click, every fetch, every animation on every website you've ever visited — all orchestrated by this loop, running billions of cycles on billions of devices, right now.

Watch the full animated breakdown: The JavaScript Event Loop Visualized