DEV Community

Cover image for The Secret Life of JavaScript: Asynchrony
Aaron Rose
Aaron Rose

Posted on

The Secret Life of JavaScript: Asynchrony

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," he admitted. "I’m trying to write the instructions for the 'Book Retrieval' sequence. But 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 looked up from her own work, offering a reassuring smile. She walked over to his side of the desk.

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

The Call Stack (Single-Threaded)

Margaret tapped the stack of papers right in front of Timothy.

"The JavaScript engine is single-threaded," she explained. "This means it has exactly one Call Stack. It can only execute one line of code at a time."

"So if I have a slow function?" Timothy asked.

"If you put a slow function on the Call Stack—like a network request or a heavy calculation—the engine cannot do anything else until that function finishes. It is blocked. That is why your interface freezes."

Web APIs (Offloading the Work)

"So how do we fetch data without freezing?" Timothy asked.

"We don't do the work on the Call Stack," Margaret said. "We offload it to the Web APIs."

"The Web APIs?"

"Yes. Think of them as separate departments in the browser—the Timer department, the Network department—that work independently from us. They run outside of our main single thread."

She pointed to his code.

"When you call setTimeout or fetch, you are effectively handing that task over to these departments. The browser handles the waiting or the downloading in the background. Meanwhile, JavaScript’s Call Stack immediately moves to the next line of code."

"So the Stack stays clear?"

"Exactly. The Stack stays responsive."

The Queues (Macrotasks & Microtasks)

"But here is the important part," Margaret continued. "When the Web API is finished—when the timer hits zero or the data arrives—it cannot just shove the code back onto the Stack randomly. It has to queue up."

She drew two distinct boxes on Timothy's paper.

"There are two specific queues you must remember:"

The Macrotask Queue (Callback Queue):

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

The Microtask Queue:

  • Examples: Promise.then, queueMicrotask, await.
  • Role: This is a higher-priority queue. The engine treats these as urgent.

The Event Loop (The Coordinator)

"And how does the code get from those queues back to my main program?" Timothy asked.

"That is the job of the Event Loop," Margaret said. "It is a continuous process that monitors the state of the stack and the queues."

She wrote down the order of operations:

  1. Check the Call Stack: Is it empty? If no, wait.
  2. Run Microtasks: If the Stack is empty, run everything in the Microtask Queue (Promises) until it is empty.
  3. Run Macrotasks: Only after the Microtasks are clear, run one item from the Macrotask Queue (Timers).

The "Zero Delay" Test

Timothy grabbed a fresh sheet of paper. "I want to verify the priority order. I'll write a test case."

He scribbled down a quick script:

console.log("Start");

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

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

console.log("End");

Enter fullscreen mode Exit fullscreen mode

"Okay," Timothy analyzed. "I have a setTimeout with a delay of 0 milliseconds. I also have a Promise. Which runs first?"

Margaret looked at the code. "Let's apply the rules."

  1. "Start": "Synchronous. Runs immediately on the Call Stack," she said.
  2. The Timeout: "Handed to Web APIs. It finishes instantly and goes to the Macrotask Queue."
  3. The Promise: "Handed to Web APIs. It resolves instantly and goes to the Microtask Queue."
  4. "End": "Synchronous. Runs immediately."

"Now the Stack is empty," Timothy said. "We have a Timeout in the Macro queue and a Promise in the Micro queue."

"Which queue has priority?" Margaret asked.

"The Microtask Queue," Timothy answered.

"Correct. So the Event Loop grabs the Promise first."

Timothy finished the output sequence: "So it prints 'Promise'. And only when that queue is empty does it go to the Macrotask Queue to print 'Timeout'."

// Console Output:
// Start
// End
// Promise
// Timeout

Enter fullscreen mode Exit fullscreen mode

Margaret nodded with satisfaction. "Exactly. Even with a zero-second delay, a setTimeout will always lose to a Promise, because Macrotasks must wait for Microtasks."


Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.

Top comments (0)