Event Loop in JavaScript: Visual Guide for Beginners

Event Loop in JavaScript: Visual Guide for Beginners

The JavaScript event loop lets a single-threaded runtime handle
async work. It empties the call stack, runs microtasks (Promises), then handles macrotasks (setTimeout, UI
events) so the UI stays responsive.


Introduction

Alright, let’s demystify something every JS dev must know: the event loop. Think of
JavaScript as a single chef in a busy kitchen. The chef (the JS runtime) does one thing at a time, but
orders (async tasks) keep coming. The event loop is the manager who decides which order to cook next.

If you’ve ever wondered why Promise.then() sometimes beats setTimeout(..., 0),
or why your UI freezes even without an obvious heavy loop — this article is for you. Code examples
included. Play with them in the console. I did — many times, in cafes. 🙂

Call Stack and Single Threaded Nature

The call stack is simple: last-in, first-out. When you call a function, it goes on the stack; when it
returns, it pops off. Synchronous code blocks the stack. So if you write a while-loop that never breaks,
the UI freezes. Oops.

Example: simple stack behavior

function a(){ console.log('a start'); b(); console.log('a end'); } function b(){ console.log('b'); } a();

Output order: a startba end. Simple, predictable. No
surprises here.

Tasks vs Microtasks

Here’s where people trip up. There are two important queues:

  • Microtask Queue (Jobs) — Promises (.then, async/await
    continuations), queueMicrotask.
  • Task Queue (Macrotasks)setTimeout, setInterval, DOM
    events, rendering tasks.

When the call stack empties, microtasks run before macrotasks. So Promises typically win the
race against setTimeout(..., 0).

Event Loop Step-by-Step

A simplified tick is:

  1. Run one macrotask (take from task queue, push to call stack).
  2. Let it run; synchronous code executes.
  3. When the call stack is empty, drain the microtask queue completely.
  4. Update rendering if needed.
  5. Repeat.

This is why too many microtasks can block rendering — the browser waits until microtasks finish before
painting. Annoying, yes. Avoid long microtask chains.

Code Examples

Example 1: Promise vs setTimeout ordering

console.log('script start');

setTimeout(() => console.log('setTimeout'), 0);

Promise.resolve().then(() => console.log('promise1')).then(() => console.log('promise2'));

console.log('script end');

Expected output:

script start script end promise1 promise2 setTimeout

Promises are microtasks — they run after the call stack clears but before macrotasks like
setTimeout.

Example 2: async/await is syntactic sugar over Promises

async function foo(){ console.log('foo start'); await bar(); console.log('foo end'); } function bar(){ console.log('bar'); return Promise.resolve(); }

console.log('script start');
foo();
console.log('script end');

Output:

script start foo start bar script end foo end

await pauses foo and schedules the continuation as a microtask. Neat, right?

Common Gotchas

Long microtask chains block rendering

If you enqueue lots of microtasks (for instance, Promise chains that keep scheduling more Promises), the
runtime runs them all before giving control back to the browser. So the UI can freeze even without heavy
synchronous loops. Seen this in production — not fun.

setTimeout(..., 0) is not immediate

Browsers clamp timers, and setTimeout callbacks are macrotasks. They run after microtasks —
so don’t expect them to run “immediately”.

Relying on order between different async primitives

Assuming setTimeout will run before a Promise resolution is a bug. Promises (microtasks) run
earlier.

Visualizing with a Timeline

Imagine each event-loop tick as:

  • Execute one macrotask (like handling a click).
  • Run all microtasks triggered by it.
  • Then render (if necessary).

So: macrotask → microtasks → paint. Repeat. If you want smoother animations, let the browser paint by not
starving it with microtasks.

Further Examples and Patterns

Example 3: Using queueMicrotask directly

console.log('start'); queueMicrotask(() => console.log('microtask')); setTimeout(() => console.log('timeout'), 0); console.log('end');

Output:

start end microtask timeout

queueMicrotask is a low-level way to schedule microtasks. Useful sometimes.

Practical pattern: batching updates

If you must process many items without blocking the UI, chunk the work. Process a small batch, then
setTimeout(..., 0) or requestAnimationFrame the next batch. Web Workers are
great for heavy computation — offload work entirely.

FAQ

Q1: Is JavaScript truly single-threaded?
A: The code execution environment is
single-threaded (call stack). But browsers/Node have multiple threads behind the scenes for I/O,
rendering, etc.

Q2: Which runs first — setTimeout or Promise.then?
A: Promise.then
(microtask) runs before setTimeout callbacks (macrotasks) after the current call stack
empties.

Q3: Can microtasks starve rendering?
A: Yes. If many microtasks are queued
continuously, the browser may not get a chance to render.

Q4: How to delay work without blocking UI?
A: Break work into chunks and schedule
via setTimeout, requestAnimationFrame, or use Web Workers for heavy compute.

Q5: Are web workers part of the event loop?
A: Web Workers have their own event loop
— they don’t block the main thread and communicate via messages.

Closing notes

Understanding the event loop makes debugging async behavior so much easier. Try the examples in the
browser console. Tinker. I remember breaking an app at 2 AM because I forgot that await
schedules a microtask — rookie mistake. Another time, I optimized a dashboard by batching DOM writes;
CPU drop was dramatic. Small wins, big smiles.

Go experiment. You’ll get the hang of it. Cheers.

Leave a Comment

Your email address will not be published. Required fields are marked *