The Secret Life of JavaScript: Asynchrony

Published: (December 20, 2025 at 11:01 PM EST)
3 min read
Source: Dev.to

Source: Dev.to

Introduction

Timothy sighed, resting his forehead against the cool oak of the drafting table. Sheets of logic diagrams were spread out before him.

“I’m stuck, Margaret. I’m trying to write the instructions for the ‘Book Retrieval’ sequence. Every time the code asks the engine to fetch a massive volume of data, the whole interface freezes. It sits there, doing absolutely nothing, just waiting for the data to arrive. It’s terribly rude to the user.”

Margaret smiled reassuringly and replied:

“It is not rude, Timothy. It is simply synchronous. To fix this, you have to understand the specific architecture of the environment we are working in. It starts with the Call Stack.”

Call Stack

  • The JavaScript engine is single‑threaded, meaning it has exactly one Call Stack and can execute only one line of code at a time.
  • If a slow function (e.g., a network request or heavy calculation) is placed on the Call Stack, the engine cannot do anything else until that function finishes. This blocks the UI and causes the freeze.

Web APIs

Instead of doing the work on the Call Stack, JavaScript offloads it to the Web APIs (Timer, Network, etc.) that run outside the main thread.

  • When you call setTimeout or fetch, the task is handed to these background services.
  • The browser handles waiting or downloading in the background while the Call Stack proceeds to the next line of code.

Task Queues

When a Web API finishes its work, it cannot push code back onto the Call Stack arbitrarily. It must place the callback into one of two queues:

Macrotask Queue (Callback Queue)

  • Examples: setTimeout, setInterval, I/O operations
  • Role: Holds standard asynchronous tasks.

Microtask Queue

  • Examples: Promise.then, queueMicrotask, await
  • Role: Higher‑priority queue; the engine treats these as urgent.

Event Loop

The Event Loop continuously monitors the Call Stack and the queues:

  1. Check the Call Stack: If it’s not empty, wait.
  2. Run Microtasks: If the Stack is empty, run everything in the Microtask Queue until it’s empty.
  3. Run Macrotasks: After all microtasks are cleared, run one item from the Macrotask Queue.

Example: Priority Order

Timothy wrote a test case to verify the priority order:

console.log("Start");

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

Promise.resolve().then(function() {
    console.log("Promise");
});

console.log("End");

Execution Walkthrough

  1. console.log("Start") – Synchronous, runs immediately on the Call Stack.
  2. setTimeout(..., 0) – Handed to Web APIs, finishes instantly, and its callback goes to the Macrotask Queue.
  3. Promise.resolve().then(...) – Handed to Web APIs, resolves instantly, and its callback goes to the Microtask Queue.
  4. console.log("End") – Synchronous, runs immediately.

After the synchronous code finishes, the Call Stack is empty. The Event Loop now processes the queues:

  • Microtask Queue has the Promise callback → runs first.
  • Macrotask Queue has the setTimeout callback → runs after the microtasks are cleared.

Console Output

Start
End
Promise
Timeout

Even with a zero‑second delay, a setTimeout will always lose to a Promise because macrotasks must wait for microtasks to finish.

Back to Blog

Related posts

Read more »