Advanced Async Patterns

Course: JavaScript Advanced

Lesson 9: Advanced Async Patterns

Level

Advanced

Estimated Reading Time

10–15 minutes

Goal

By the end of this lesson, the learner will be able to:

  • Use Promise.all to fetch multiple resources concurrently and handle partial failures
  • Implement cancellable requests with AbortController to prevent stale responses
  • Identify and avoid race conditions in async UI updates

Frontend Development Context

Most real frontend applications need to fetch data from multiple endpoints simultaneously — a dashboard might need user data, notifications, and analytics all at once. Doing these sequentially with await wastes time; doing them concurrently with Promise.all can cut load time dramatically. But concurrency also introduces new failure modes: what happens when one request fails? What happens when a user navigates away mid-fetch and the response arrives after they've moved on?

Advanced async patterns are what separate functional prototypes from production-quality frontends. Handling concurrent requests, cancelling stale fetches, avoiding memory leaks from responses that arrive too late, and dealing with race conditions in search-as-you-type inputs are real problems every frontend developer faces. This lesson teaches the tools and patterns to handle them correctly.

Explanation

Promise.all(promises) takes an array of promises and returns a single promise that resolves when all of them resolve, with an array of their results in the same order. If any promise rejects, Promise.all immediately rejects with that rejection reason and ignores the others. This fail-fast behavior is appropriate when you need all responses to render the UI — if any one fails, the whole operation is meaningless.

However, fail-fast is not always what you want. If some parts of a page are independent, failing to fetch one section shouldn't block the others. For these cases, combine Promise.all with individual try/catch blocks or use Promise.allSettled (covered in the next lesson) to handle each result individually regardless of success or failure.

AbortController is the browser API for cancelling fetch requests. You create a controller, pass its signal to fetch, and call controller.abort() whenever you need to cancel. The fetch promise rejects with an AbortError. Cancelling stale requests is essential in search-as-you-type (where a new keystroke should cancel the in-flight request from the previous keystroke) and in component cleanup (where navigating away should cancel any pending fetches).

Race conditions occur when two async operations are in flight simultaneously and the one that started later resolves first. In a search field, if the user types "ja" and then "jav", the "jav" request might resolve before the "ja" request. If the UI renders whatever resolves last, it will display results for "ja" — the wrong query. The fix is to track which request is "current" and ignore responses from stale ones.

Combining AbortController with a request ID or generation counter provides robust race-condition prevention. When a new request starts, abort the previous one. The AbortError from the cancelled request is caught and ignored, and only the latest response ever updates the UI.

Code Example

This example shows Promise.all for a dashboard that fetches three independent endpoints, with AbortController cancelling stale search requests to prevent race conditions.

// Simulate fetch with controllable delay
function fakeFetch(url, signal, delay = 200) {
  return new Promise((resolve, reject) => {
    const timeout = setTimeout(() => {
      resolve({ url, data: `data from ${url}` });
    }, delay);

    signal?.addEventListener("abort", () => {
      clearTimeout(timeout);
      reject(new DOMException("Aborted", "AbortError"));
    });
  });
}

// --- Promise.all: fetch dashboard sections concurrently ---
async function loadDashboard() {
  const controller = new AbortController();
  const { signal } = controller;

  // Optional: cancel all if user navigates away
  // window.addEventListener("beforeunload", () => controller.abort());

  try {
    const [user, notifications, analytics] = await Promise.all([
      fakeFetch("/api/user",          signal, 150),
      fakeFetch("/api/notifications", signal, 200),
      fakeFetch("/api/analytics",     signal, 180),
    ]);

    console.log("Dashboard loaded:");
    console.log(" User:", user.data);
    console.log(" Notifications:", notifications.data);
    console.log(" Analytics:", analytics.data);
  } catch (err) {
    if (err.name === "AbortError") {
      console.log("Dashboard fetch cancelled");
    } else {
      console.error("Dashboard fetch failed:", err.message);
    }
  }
}

// --- AbortController: prevent race conditions in search ---
let currentSearchController = null;

async function search(query) {
  // Cancel the previous in-flight request
  if (currentSearchController) {
    currentSearchController.abort();
  }

  currentSearchController = new AbortController();
  const { signal } = currentSearchController;

  try {
    const result = await fakeFetch(`/api/search?q=${query}`, signal, 300);
    console.log(`Search result for "${query}":`, result.data);
    // Safe: only the latest request reaches here
  } catch (err) {
    if (err.name === "AbortError") {
      console.log(`Search for "${query}" was cancelled (superseded)`);
    } else {
      console.error("Search error:", err.message);
    }
  }
}

loadDashboard();

// Simulate rapid search input — first two should be cancelled
search("j");
search("ja");
search("jav"); // only this one should complete

Code Explanation

fakeFetch simulates a real fetch call: it resolves after a delay and listens to the AbortSignal. When abort() is called, the timeout is cleared and the promise rejects with an AbortError. This mirrors exactly how fetch behaves with a real signal.

loadDashboard uses Promise.all to start all three requests simultaneously. All three begin at the same moment and the dashboard loads as soon as the slowest one resolves (200ms for notifications), rather than waiting for each to finish sequentially (150 + 200 + 180 = 530ms). If any request is aborted or fails, the catch block checks for AbortError and handles it differently from network errors.

search demonstrates the critical pattern for race condition prevention. currentSearchController holds the controller for the currently active request. Every time search is called with a new query, it aborts the previous request (if one exists) and creates a fresh controller. The aborted request's catch block sees AbortError and returns silently. Only the latest request — the one for "jav" — ever reaches the console.log after await.

This pattern is used directly in production search implementations, React's useEffect cleanup functions, and any component that fetches data and might be unmounted before the response arrives.

Pattern Highlights

Positive Pattern: Always check for AbortError specifically in your catch blocks when using AbortController — treating a cancellation the same as a network error will log false failures and potentially show error UI to users who simply typed another character.

⚠️ Neutral Note: Promise.all fails fast on the first rejection. If your UI can partially render while some sections fail, use individual try/catch around each fetch or use Promise.allSettled instead, which waits for all promises and reports each result individually.

Negative Pattern: Do not await sequential requests that could run in parallel — awaiting each fetch one after another in a for loop makes the total wait time the sum of all delays; Promise.all makes it the maximum of the longest delay.

Common Mistakes

Mistake 1: Awaiting requests sequentially instead of concurrently

Using await before each request forces them to run one at a time.

async function loadPage() {
  const user = await fetch("/api/user");          // waits ~200ms
  const posts = await fetch("/api/posts");        // waits another ~300ms
  const comments = await fetch("/api/comments"); // waits another ~150ms
  // Total: ~650ms
}

These requests are independent. Use Promise.all to run them concurrently and the total wait is only ~300ms (the longest request).

Mistake 2: Not cleaning up fetch when a component unmounts

In a React useEffect, if you don't abort the fetch on cleanup, a response from an unmounted component can try to update state and cause a warning (or worse, incorrect UI state).

useEffect(() => {
  fetch("/api/data")
    .then(res => res.json())
    .then(data => setData(data)); // may run after unmount
}, []);
// Missing: cleanup function with AbortController

Create an AbortController in the effect, pass its signal to fetch, and return a cleanup function that calls controller.abort().

Mistake 3: Not handling the race condition in search

Starting a new fetch without cancelling the old one means stale results can overwrite fresh ones.

async function search(query) {
  const result = await fetch(`/api/search?q=${query}`);
  const data = await result.json();
  renderResults(data); // may render old results if a newer fetch resolved first
}

Without aborting the previous fetch, queries "a", "ab", "abc" can all be in flight simultaneously, and whichever resolves last wins — which may be "ab" instead of "abc". Always abort the previous controller before starting a new request.

Quick Recap

  • Promise.all runs multiple promises concurrently and resolves when all succeed or rejects immediately when any one fails — reducing total wait time from the sum of delays to the maximum.
  • AbortController provides a signal that can be passed to fetch and other APIs; calling controller.abort() cancels in-flight requests and rejects with AbortError.
  • Race conditions occur when multiple async operations are in flight and an older one resolves after a newer one — prevent this by aborting the previous request each time a new one starts.
  • Always distinguish AbortError from genuine errors in catch blocks — a cancelled request is not a failure and should not be reported as one.

Ready to test your understanding? Take the quiz to reinforce what you learned.

© 2026 Ant Skillsv.26.07.07-23:01
Ant Skills - Advanced Async Patterns