Chapter 1 of ?
js 10 min read

JavaScript Mastery — Chapter 25: Promises, Async/Await & Event Loop

Module 6: Asynchronous JavaScript & Workers Chapter 25 32 min read

Promises, Async/Await & Event Loop

Demystify the engine driving modern asynchronous JavaScript. Master Promise states, chaining, async/await syntax, and the definitive architectural mental model: the Event Loop, Call Stack, Microtask Queue, and Task Queue.

The Event Loop: Microtasks vs. Macrotasks Execution Engine

The fundamental rule of JavaScript concurrency: The Event Loop drains ALL pending Microtasks (Promises) before picking even ONE single Macrotask (setTimeout)!

EVENT LOOP 1. CALL STACK console.log('Sync End') mainScript() Must be EMPTY before any queue can run! 2. MICROTASK QUEUE (High Priority) • Promise.then() / .catch() • queueMicrotask(fn) ⚡ Drained to 0 before Macrotasks! 3. MACROTASK QUEUE (Task) • setTimeout / setInterval • DOM UI Click Events / I/O Processed 1 at a time per tick

1. The Promise Architecture

A Promise represents the eventual completion (or failure) of an asynchronous operation and its resulting value. Unlike callbacks, a Promise provides three foundational guarantees:

1. Pending
Initial state.
The operation is ongoing. Not fulfilled or rejected yet.
2. Fulfilled (Resolved)
Completed successfully.
Has an immutable resolved value. Calls attached .then() handlers.
3. Rejected
Operation failed.
Has a rejection reason (Error). Triggers attached .catch() handlers.
The Immutability Guarantee
Once a Promise transitions from Pending to either Fulfilled or Rejected, it is settled and permanently immutable. Calling resolve() or reject() a second time has zero effect, completely eliminating the Inversion of Control double-charge hazard!

Constructing and Consuming a Promise

function fetchOrder(orderId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (orderId <= 0) {
        return reject(new Error('Invalid Order ID'));
      }
      resolve({ id: orderId, total: 249.99, status: 'PAID' });
    }, 200);
  });
}

// Chaining .then(), .catch(), and .finally():
fetchOrder(101)
  .then(order => {
    console.log('Order received:', order.total);
    return order.id; // Return value is wrapped in a new fulfilled Promise!
  })
  .then(id => {
    console.log('Processing confirmation for order #', id);
  })
  .catch(err => {
    console.error('Error occurred in pipeline:', err.message);
  })
  .finally(() => {
    console.log('Cleanup: Operation completed regardless of success or failure.');
  });
Try it in Playground

2. async / await: Synchronous Elegance

Introduced in ES2017, async / await is syntactic sugar built on top of native Promises, allowing asynchronous code to be structured with clean, linear readability:

Production async / await Flow

async function processCustomerCheckout(userId, cart) {
  try {
    // 1. Await pauses this function's execution without blocking the browser thread:
    const user = await fetchUserProfile(userId);
    const invoice = await createInvoice(user, cart);
    const payment = await chargePayment(invoice.total);

    return { success: true, paymentId: payment.id };
  } catch (err) {
    // Standard structured try...catch catches ANY rejection in the sequence!
    console.error('Checkout failed:', err.message);
    throw err; // Re-throw to caller
  } finally {
    console.log('Checkout flow completed.');
  }
}
Try it in Playground

Interactive Mini-Lab: Microtask vs. Macrotask Execution Tracer

Predict the output order of the following snippet, then click "Step Through Event Loop" to watch the V8 engine process synchronous frames, microtasks, and macrotasks in real time:

console.log('1: Sync Start');
setTimeout(() => console.log('2: Macrotask setTimeout'), 0);
Promise.resolve().then(() => console.log('3: Microtask Promise 1'))
                 .then(() => console.log('4: Microtask Promise 2'));
queueMicrotask(() => console.log('5: Microtask queueMicrotask'));
console.log('6: Sync End');
Call Stack
Microtask Queue
Macrotask Queue
Console Output

Hands-on Challenge: Exponential Backoff Retry Utility

Coding Challenge

Real-world microservices experience transient network hiccups. Write an asynchronous retryWithBackoff(fn, maxRetries, baseDelayMs) function that automatically retries failed asynchronous calls with exponential backoff delay (e.g. 100ms → 200ms → 400ms):

Retry System Requirements
  1. Use async / await syntax with clean try...catch loops.
  2. If fn() succeeds, resolve immediately with its return value.
  3. If fn() fails, wait baseDelayMs * (2 ** attempt) before retrying.
  4. If all retries are exhausted, throw the final captured error.
Production Exponential Backoff Implementation
/**
 * Helper to pause execution asynchronously using Promise + setTimeout:
 */
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

/**
 * Executes an async operation with automated exponential backoff retries.
 */
async function retryWithBackoff(asyncFn, maxRetries = 3, baseDelayMs = 200) {
  let lastError;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await asyncFn();
    } catch (err) {
      lastError = err;
      if (attempt === maxRetries) break;

      const delay = baseDelayMs * (2 ** attempt);
      console.warn(`[Attempt ${attempt + 1} Failed] Retrying in ${delay}ms... Error: ${err.message}`);
      await sleep(delay);
    }
  }

  throw new Error(`All ${maxRetries} retries exhausted. Final error: ${lastError.message}`);
}

// Verification:
let callCount = 0;
const flakyApi = async () => {
  callCount++;
  if (callCount < 3) throw new Error('503 Service Unavailable');
  return { status: 200, data: 'Mission Success!' };
};

retryWithBackoff(flakyApi, 4, 100).then(res => console.log('Resolved:', res));

Chapter 25 Knowledge Check

Validate your mastery of Promises, async/await, and Event Loop queue scheduling.

1. In what order does the Event Loop process tasks between the Microtask Queue and the Macrotask (Task) Queue?
It executes one Macrotask, then one Microtask alternately in a round-robin cycle.
It drains ALL pending Microtasks completely before executing the next Macrotask.
Macrotasks always take priority over Microtasks.
Microtasks are only processed when the browser window is minimized.
2. What always happens when an async function returns a primitive value like return 42;?
It returns the raw number 42 synchronously.
The JavaScript engine automatically wraps 42 in a fulfilled Promise: Promise.resolve(42).
It throws a TypeError because async functions must return an object.
It blocks until await is called on the outside.
3. What is the output order of: console.log('A'); Promise.resolve().then(() => console.log('B')); setTimeout(() => console.log('C'), 0); console.log('D');?
A, B, C, D
A, D, B, C
A, D, C, B
C, B, A, D
4. Can a Promise change its state after it has already been fulfilled or rejected?
Yes, calling resolve() again updates the fulfilled value.
No, once settled (fulfilled or rejected), a Promise's state and value are permanently immutable.
Yes, but only inside a .finally() block.
Only in Node.js environments.
5. When await fetch(...) is encountered inside an async function, what happens to the main browser thread?
The entire browser UI thread completely freezes until the network response arrives.
Only the async function's execution is paused; the main thread continues executing other scripts, handling button clicks, and repainting animations freely.
The browser spawns a brand new Web Worker thread automatically.
The fetch call is immediately canceled if it takes longer than 16 milliseconds.
Done with this chapter?
Mark it complete to track your progress and unlock your certificate.
Next Up
—

Learner Reviews

Write a Review
Share your experience to help other learners.
Your Rating *
★ ★ ★ ★ ★