Skip to main content

Command Palette

Search for a command to run...

Understanding JavaScript’s Event Loop, Web APIs, and Task Queues

Published
5 min read
Understanding JavaScript’s Event Loop, Web APIs, and Task Queues

JavaScript is often described as single-threaded but non-blocking — meaning it can only execute one task at a time, yet it somehow manages to handle multiple asynchronous operations like timers, network requests, and user interactions without freezing.

So how does this magic happen?

It’s all thanks to the Event Loop, Web APIs, and Task Queues working together to manage how JavaScript schedules and executes code efficiently.

JavaScript Runtime Environment

Before we dive into the Event Loop and queues, it’s important to understand where all this actually happens — inside the JavaScript Runtime Environment.

JavaScript doesn’t run in isolation. It needs an environment — like a web browser (Chrome, Firefox) or Node.js — that provides additional features and APIs beyond the core language.

What the Runtime Includes

The runtime environment typically consists of three main components:

  1. JavaScript Engine
    This is where the actual JavaScript code is executed.
    For example:

    • Chrome → V8

    • Firefox → SpiderMonkey

    • Safari → JavaScriptCore

Inside the engine, two key parts do the heavy lifting:

  • Heap → where memory is allocated

  • Call Stack → where function calls are managed

  1. Web APIs (or C++ APIs in Node.js)
    These are provided by the runtime, not the JS language itself.

    • In browsers → DOM, setTimeout, fetch, console, etc.

    • In Node.js → fs, http, process, etc.

These APIs allow JavaScript to perform asynchronous operations and interact with the outside world.

  1. Callback Queues and the Event Loop
    These manage the flow of tasks between the engine and the APIs — ensuring asynchronous operations run in the correct order.

Putting It All Together

Here’s a simplified view of how the runtime works:

┌────────────────────────────────────────────┐
│       JavaScript Runtime Environment       │
│────────────────────────────────────────────│
│  JavaScript Engine (Call Stack + Heap)     │
│  Web APIs (Timers, DOM, fetch, etc.)       │
│  Callback Queues (Micro & Macro)           │
│  Event Loop (coordinates everything)       │
└────────────────────────────────────────────┘

When you execute JavaScript:

  • The engine runs synchronous code.

  • Web APIs handle async tasks in the background.

  • The Event Loop and Queues decide when those async callbacks should re-enter the main thread.

This ecosystem allows JavaScript to appear concurrent — even though it’s single-threaded at its core.

1. The Call Stack

The Call Stack is the heart of JavaScript’s execution.
It keeps track of which function is currently running and what should run next.

When you call a function, it’s pushed onto the stack.
When it finishes executing, it’s popped off.

The stack works on the LIFO principle — Last In, First Out.

Example:

function first() {
  console.log("First");
}

function second() {
  first();
  console.log("Second");
}

second();

Execution Flow:

  1. second() is pushed onto the stack.

  2. Inside second(), first() is pushed.

  3. first() logs "First" → then is popped off.

  4. Then console.log("Second") runs.

Output:

First
Second

This is how JavaScript executes synchronous code — one step at a time, top to bottom.

2. Web APIs

JavaScript alone can’t handle asynchronous operations (like timers, HTTP requests, or event listeners).
That’s where Web APIs — features provided by the browser environment — come in.

Examples include:

  • setTimeout() and setInterval()

  • fetch() / XMLHttpRequest

  • addEventListener()

When you use these APIs, JavaScript delegates them to the browser.
This frees up the call stack while the browser handles the async task in the background.

Once complete, the browser moves the callback to a Task Queue, ready to run later when the call stack is empty.

3. The Event Loop

The Event Loop acts like a diligent manager checking in on two things:

  1. Is the Call Stack empty?

  2. Are there any callbacks waiting in the Task Queues?

If the stack is clear, the Event Loop picks the next callback (from the appropriate queue) and pushes it onto the stack to execute.

The Event Loop ensures asynchronous operations never interrupt the running synchronous code.

This mechanism makes JavaScript non-blocking and keeps your applications responsive.

4. Task Queues

JavaScript uses two main queues to manage async callbacks:

Microtask Queue

  • Handles:

    • Promise.then() and Promise.catch()

    • MutationObserver

    • queueMicrotask()

  • Higher priority — runs right after the current call stack clears.

Macro-task Queue (Task Queue)

  • Handles:

    • setTimeout()

    • setInterval()

    • DOM events

    • Network request callbacks

  • Lower priority — runs after all microtasks have finished.

Execution Order:

Call Stack
   ↓
Microtask Queue (Promises)
   ↓
Macro-task Queue (Timers, Events)

So, microtasks always run before any macrotask.

5. How It All Comes Together

console.log('Start'); 

setTimeout(() => console.log('Timeout 1'), 0); // Macro-task

Promise.resolve().then(() => console.log('Promise 1')); // Micro-task

console.log('End');

Step-by-Step Execution:

  1. console.log('Start') runs → logs Start.

  2. setTimeout() → handled by the Web API → its callback is placed in the macro-task queue.

  3. Promise.resolve().then(...) → callback added to the microtask queue.

  4. console.log('End') runs → logs End.

  5. Call stack is now empty.

    • Event Loop runs microtasks first → logs Promise 1.

    • Then it executes macro-tasks → logs Timeout 1.

Start
End
Promise 1
Timeout 1

Visualizing the Flow

┌────────────────────────────┐
│        Call Stack          │
│  Executes code line-by-line│
└─────────────┬──────────────┘
              │
     Event Loop monitors
              ↓
┌────────────────────────────┐
│  Microtask Queue (Promises)│
│  Higher priority           │
└─────────────┬──────────────┘
              ↓
┌────────────────────────────┐
│   Macro-task Queue (Timers)│
│   Lower priority           │
└────────────────────────────┘

Extended Example — Mixed Tasks

Let’s expand the earlier example to show how multiple microtasks and macrotasks interleave:

console.log("Start");

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

Promise.resolve().then(() => {
  console.log("Promise 1");
  setTimeout(() => console.log("Timeout 2"), 0);
});

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

console.log("End");

Breakdown:

  1. Logs Start.

  2. setTimeout (→ macro-task).

  3. Two Promises (→ microtasks).

  4. Logs End.

  5. Stack clears → Event Loop runs:

    • Microtasks → "Promise 1", "Promise 2".
      During "Promise 1", another timer (Timeout 2) is queued.
  6. Then runs macrotasks in order → "Timeout 1", then "Timeout 2".

Start
End
Promise 1
Promise 2
Timeout 1
Timeout 2

Conclusion

The Event Loop is what makes JavaScript powerful, efficient, and responsive — despite being single-threaded.
By offloading work to Web APIs and carefully managing Task Queues, JavaScript achieves concurrency without true parallelism.

More from this blog

Aanchal's blog

40 posts