Front Runner Front End Web Development Blog

JavaScript Event Loop Explained Simply

JavaScript event loop explained simply for front-end developers. Learn call stack, Web APIs, microtasks, macrotasks, and rendering timing.

| June 18, 2026 | 8 min read

You click a button, kick off a fetch, add a setTimeout, and somehow your UI still responds without your JavaScript turning into a tangled mess. That bit of browser magic is exactly why “javascript event loop explained” keeps showing up in search bars. It sounds abstract at first, but once it clicks, a lot of weird timing bugs stop feeling like witchcraft.

JavaScript event loop explained in plain English

JavaScript runs on a single thread for most of the code you write in the browser. That means it can do one thing at a time on its main call stack. So the obvious question is: if JavaScript can only handle one task at once, how does it deal with timers, user clicks, network requests, and rendering updates without freezing everything?

That is where the event loop comes in. It coordinates what runs now, what waits, and what gets picked up next. It is less like raw speed and more like queue management with a clipboard and a mild caffeine dependency.

To understand it, you only need four moving parts: the call stack, Web APIs, the task queue, and the microtask queue.

The call stack

The call stack is where JavaScript executes functions. When you call a function, it gets pushed onto the stack. When it finishes, it gets popped off. If another function is called inside it, that new one goes on top.

This is the bit doing actual work right now. If the stack is busy, nothing else runs. That includes your click handlers, timer callbacks, and promise callbacks. They all have to wait their turn.

Web APIs

Browsers provide features JavaScript can use, such as setTimeout, DOM events, and fetch. These are often grouped under “Web APIs”. When you call setTimeout, for example, the timer does not sit on the call stack counting seconds like a tiny intern with a stopwatch. The browser handles it elsewhere.

Once the timer finishes, its callback becomes eligible to run. Eligible does not mean immediate. It still has to wait until the stack is clear and the event loop moves it back into JavaScript execution.

Task queue

The task queue, often called the macrotask queue, holds callbacks waiting to run. Typical examples include setTimeout callbacks, setInterval callbacks, and many event handlers.

When the call stack is empty, the event loop can take the next task from this queue and push it onto the stack.

Microtask queue

This queue is the one that catches people out. Promise callbacks, queueMicrotask, and MutationObserver callbacks go here.

Microtasks have higher priority than regular tasks. After the current script finishes, the event loop processes all queued microtasks before moving on to the next task. That “all” matters. If microtasks keep queueing more microtasks, they can delay everything else, including rendering.

A simple mental model

Think of the event loop as repeating this process:

  1. Run whatever is on the call stack.
  2. When the stack is empty, run all microtasks.
  3. If the browser is ready, it may render.
  4. Take one task from the task queue.
  5. Repeat.

That is simplified, but it is useful. If you remember only one thing, remember this: microtasks run before the next task.

What actually happens in code

Here is the classic example:

“`js console.log(‘A’);

setTimeout(() => { console.log(‘B’); }, 0);

Promise.resolve().then(() => { console.log(‘C’); });

console.log(‘D’); “`

The output is:

“`js A D C B “`

Why? `A` logs immediately. `setTimeout(…, 0)` hands its callback to the browser, which places it in the task queue after the timer expires. The promise callback goes into the microtask queue. `D` logs immediately because it is still part of the current script.

Once the main script finishes, the stack is empty. The event loop checks microtasks first, so `C` runs before `B`. Zero milliseconds does not mean “right now”. It means “earliest possible time after the current work and microtasks are done”. Sneaky, but legal.

Why this matters in front-end work

If you are building interfaces, the event loop affects responsiveness, rendering, and timing. It is not just interview bait.

A long-running function blocks the call stack. While that happens, the browser cannot run your click handler, process the promise callback that updates state, or repaint the screen. If a page feels janky, the event loop is usually somewhere in the crime scene photos.

This also explains why breaking heavy work into smaller chunks can help. If you split a big loop into smaller tasks with `setTimeout`, `requestAnimationFrame`, or another scheduling approach, you give the browser breathing room to process input and paint updates.

setTimeout is not a precision tool

A common misunderstanding is treating `setTimeout(fn, 1000)` as “run exactly one second later”. It means “do not run before one second, then wait until the stack is clear and the callback reaches the front of the queue”.

So if your main thread is blocked for three seconds, that one-second timer will not magically interrupt it. JavaScript is single-threaded, not telepathic.

This is why timers can drift, especially in busy tabs, background tabs, or animation-heavy pages.

Promises, async/await, and the event loop

`async/await` often looks synchronous, which is nice for humans and dangerous for assumptions. Under the hood, promise continuations still use the microtask queue.

For example:

“`js async function run() { console.log(‘Start’); await Promise.resolve(); console.log(‘After await’); }

console.log(‘One’); run(); console.log(‘Two’); “`

The output is:

“`js One Start Two After await “`

The code after `await` does not continue immediately. It gets scheduled as a microtask. That is why `Two` appears first.

This matters when you update state, read the DOM, or expect a particular order of execution. If something “should have happened already” but has not, there is a fair chance a microtask queue is involved, quietly judging you.

Rendering does not happen whenever you fancy

Browsers generally render between tasks, not in the middle of a long JavaScript execution block. So if you do this:

“`js element.textContent = ‘Loading…’; doVeryHeavyWork(); “`

The user may not see “Loading…” before the heavy work starts, because the browser has not had a chance to paint yet.

That catches developers all the time. You update the DOM and expect instant visual feedback, but the main thread remains busy, so the browser waits.

If you need the UI to update before heavy work begins, you may need to defer that work so the browser can render first. `requestAnimationFrame` or even a scheduled task can help, depending on what you are doing.

Microtasks are powerful, but they can bite

Because microtasks run before the next task, they are useful for small follow-up work that should happen soon after the current code finishes. That is why promises feel so immediate.

But overusing them can starve the task queue. If your code keeps adding microtasks inside microtasks, the browser may delay user input handling and rendering. Technically efficient, practically annoying.

So the trade-off is simple: microtasks are great for tight sequencing, but not for endless chains of work. Use them with intent, not like confetti.

A quick note on Node.js

If you learned JavaScript outside the browser, be aware that Node.js has an event loop too, but the details differ. The broad ideas are similar, yet the phases and APIs are not identical.

For front-end work, focus on browser behaviour first. That is where things like rendering, DOM events, and Web APIs matter most.

How to debug event loop confusion

When timing feels off, stop guessing and ask a few boring but effective questions. Is the code running immediately on the stack, as a microtask, or as a task? Is the main thread blocked by heavy synchronous work? Are you expecting a render before the browser has had a chance to paint?

Adding simple `console.log` statements around promises, timers, and synchronous code can reveal the order quickly. Browser DevTools performance recordings are even better if the issue involves jank or delayed paints.

Most event loop bugs are not really bugs in the loop. They are mismatches between what we expect the browser to do and what it is actually allowed to do.

The version worth remembering

If you want the shortest useful version of javascript event loop explained, it is this: JavaScript runs one thing at a time on the call stack, the browser handles async work outside that stack, and the event loop decides what comes back next. Microtasks run before regular tasks, and rendering usually waits until JavaScript gives the browser a moment to breathe.

Once you see that pattern, a lot of front-end behaviour becomes less mysterious. And when your next `setTimeout(…, 0)` still runs later than expected, you can blame the queue with confidence instead of glaring at your laptop like it has personally betrayed you.

The event loop is one of those fundamentals that pays rent for years, so it is worth getting comfortable with, even if it initially sounds like browser bureaucracy with extra steps.