The JavaScript Event Loop: A Beginner's Guide

Abstract image used to represent JavaScript Event Loop: A Beginner’s Guide
Image by Gift Habeshaw.

In Brief

The event loop lets JavaScript finish the current call stack, drain queued microtasks such as promise reactions, allow the browser to render when appropriate, and then take the next task. Timers therefore set a minimum delay rather than an exact execution time. Longrunning JavaScript still blocks progress, because the next queued work cannot run until the stack is clear.

When a resolved promise callback runs before setTimeout(..., 0), the event loop can look as though it has ignored the order of the code. It has not. The call stack, microtask queue, task queue, and browser rendering opportunities each have a defined place, and writing them down is often enough to explain an ordering bug.


What is the JavaScript Event Loop?

JavaScript is singlethreaded, meaning that it can execute one piece of code at a time. However, it manages to handle asynchronous tasks (like HTTP requests, timers, or user interactions) seamlessly. The event loop is the mechanism that allows JavaScript to:

  • Execute code.
  • Collect and process events.
  • Run queued tasks.

The event loop schedules queued work when JavaScript is ready to run it. A longrunning callback can still block the thread and delay everything waiting behind it.


The Building Blocks of the Event Loop

The Call Stack

The call stack is a data structure that keeps track of function calls. When a function is invoked, it's added to the top of the stack. Once it completes, it is removed from the stack.

const first = () => {
  console.log("First");
  second();
};

const second = () => {
  console.log("Second");
};

first();

Execution Flow:

To illustrate this, the example above executes like this:

  1. first() is pushed onto the stack.
  2. Inside first(), second() is called and added to the stack.
  3. second() finishes and is popped off the stack.
  4. first() completes and is popped off the stack.

Web APIs

In browsers, Web APIs handle asynchronous operations like setTimeout, DOM events, and fetch. These APIs run outside of the JavaScript engine and interact with the event loop. For example:

console.log("Start");

setTimeout(() => {
  console.log("Timeout");
}, 0);

console.log("End");

// Outputs:
// Start
// End
// Timeout

Even with a delay of 0, setTimeout is handled asynchronously, allowing synchronous code (console.log("End")) to execute first.

Task Queue (Callback Queue)

For APIs that queue tasks, such as setTimeout, the callback waits in a task queue until the event loop can run it. The current JavaScript must finish first. Promise reactions follow a different route: their callbacks are queued as microtasks, as the next example shows.

Microtasks Queue

Promise reactions and MutationObserver callbacks run as microtasks. At a microtask checkpoint, queued microtasks are drained before the event loop takes another task.

console.log("Start");

Promise.resolve().then(() => {
  console.log("Microtask");
});

console.log("End");

// Outputs:
// Start
// End
// Microtask

In this example, the promise reaction is queued as a microtask. The promise object itself is not a queued callback.


The Event Loop in Action

Simplified Event Loop Flow:

  1. Execute synchronous code (call stack).
  2. Process microtasks.
  3. Render updates (if needed).
  4. Process tasks from the task queue.
  5. Repeat.

Example Combining All Concepts

Here's an example which (I hope) illustrates all of these concepts combined together:

console.log("1");

setTimeout(() => console.log("2"), 0);

Promise.resolve().then(() => console.log("3"));

console.log("4");

// Outputs:
// 1
// 4
// 3
// 2

Explanation:

  • 1 and 4 are synchronous, and are executed immediately.
  • The promise reaction (3) is a microtask, so its callback runs after the current stack clears.
  • setTimeout (2) is in the task queue, and is processed after microtasks.

Tasks, Microtasks, and Rendering

A useful mental model is that timer callbacks are tasks, whilst promise reactions are microtasks. After the current task finishes, the browser drains queued microtasks before moving on to the next task.

Rendering sits around that scheduling. If you keep adding microtasks, you can delay the browser from getting back to painting. That is one reason a page can feel stuck even when the code is technically asynchronous.


A Debugging Rule of Thumb

When asynchronous behaviour looks surprising, write down the call stack first, then the microtasks, then the next task. Promise callbacks usually run before setTimeout(..., 0). That one detail explains a lot of small ordering bugs.


Wrapping Up

Key Takeaways

  • JavaScript is singlethreaded but handles asynchronous tasks through the event loop.
  • The call stack runs the current JavaScript; timer callbacks wait as tasks, whilst promise reactions wait as microtasks.
  • Microtasks are processed before the next task, including a task queued by setTimeout.
  • Understanding the event loop helps debug, optimise performance, and avoid pitfalls like callback hell.

When asynchronous ordering is surprising, write down the current call stack, drain the microtasks, and then move to the next task. Remember that a longrunning callback still blocks the agent. That small model is more useful in debugging than treating every asynchronous API as though it runs at the same time.


Want to find out more?

If you need senior handson support with a complex React or Next.js platform, migration, performance issue, or technical SEO problem, send me the context and I'll tell you where I can help.