Concurrency

JavaScript is single-threaded. This means it has one call stack and can do one thing at a time.

If JS is single-threaded, how can we fetch data, set timers, and listen for clicks without freezing the entire page? The answer lies in the Concurrency Model and the Event Loop.

1. The Engine Room: The Event Loop

To understand concurrency, you must understand the architecture of the browser runtime.

The Key Players

  1. Call Stack: Where code runs immediately. LIFO (Last In, First Out).

  2. Web APIs: Browser features (fetch, DOM events, setTimeout) that run outside the main JS thread.

  3. Callback Queue (Task Queue): A waiting area for callbacks from Web APIs (e.g., setTimeout finished).

  4. Microtask Queue: A VIP waiting area for Promises. It has higher priority than the Callback Queue.

  5. The Event Loop: An infinite loop that checks: Is the Stack empty? If yes, move the first item from the Queue to the Stack.

The Flow (Visualized)

  1. JS executes sync code on the Stack.

  2. Async operation (setTimeout) is handed off to Web APIs.

  3. Sync code continues.

  4. When Web API finishes, it drops the callback into the Queue.

  5. Once the Stack is empty, the Event Loop pushes the callback onto the Stack.

2. Phase 1: Callbacks (The Old Ways)

Before ES6, callbacks were the only way to handle async operations. A callback is simply a function passed into another function to be executed later.

The Pattern

Output:

  1. Start

  2. End

  3. Data Received (after 2 seconds)

The Problem: Callback Hell

When you need to perform sequential async operations (get ID -> get User -> get Posts), callbacks nest deeply, creating the "Pyramid of Doom."

3. Phase 2: Promises (The Revolution)

Introduced in ES6 (2015), a Promise is an object representing the eventual completion (or failure) of an asynchronous operation.

The 3 States

  1. Pending: Initial state, neither fulfilled nor rejected.

  2. Fulfilled (Resolved): Operation completed successfully.

  3. Rejected: Operation failed.

The Syntax

Promises flatten the pyramid using .then() chains.

The Microtask Queue Priority

Promises use the Microtask Queue, which runs immediately after the current script and before the next standard task (like setTimeout).

4. Phase 3: Async/Await (The Present)

Introduced in ES2017, async/await is syntactic sugar built on top of Promises. It allows you to write asynchronous code that looks and behaves like synchronous code.

The Rules

  1. async: Put in front of a function. It ensures the function returns a Promise.

  2. await: Put in front of a Promise. It pauses the execution of the function until the Promise resolves.

The Example (Refactoring Promises)

5. Advanced: Parallel Concurrency

Sometimes you don't want to wait for A, then B, then C. You want A, B, and C to start at the same time.

Promise.all()

Fails if any promise fails. Ideal for dependent data.

Promise.allSettled()

Waits for all to finish, regardless of success or failure.

Last updated