Skip to content
GeeksSmith
advanced 8 min read

API Request Patterns: Cancellation, Deduplication & Race Conditions

Advanced API client patterns solve race conditions, redundant bandwidth consumption, and stale responses using AbortController, promise memoization/deduplication, request pooling, and sequence tokenization.

Why it matters in interviews

The autocomplete search race condition ('apple' vs 'banana') and duplicate button click bugs are asked in nearly 100% of Senior/Lead frontend interviews. Candidates must write robust cancellation and deduplication primitives from scratch.

Visual & Interactive Explanation

Autocomplete Race Condition: Without vs With Cancellation

Sequence & Data Exchange Flow

User
Client Component
Network / API
#1
UserClient Component
request

Types 'apple'

#2
Client ComponentNetwork / API
request

Dispatch request #1 (slow, 800ms)

#3
UserClient Component
request

Types 'banana' (200ms later)

#4
Client ComponentClient Component
compute

controller.abort() on request #1

#5
Client ComponentNetwork / API
request

Dispatch request #2 (fast, 150ms)

#6
Network / APIClient Component
response

Request #2 resolves 'banana' results

#7
Client ComponentUser
render

Render 'banana' dropdown (Correct!)

Code Examples & Implementation

Abortable Autocomplete Hook (Cancels Obsolete In-Flight Searches)
import { useState, useEffect, useRef } from "react";

export function useSearch(query: string) {
  const [results, setResults] = useState<string[]>([]);
  const [loading, setLoading] = useState(false);
  const abortRef = useRef<AbortController | null>(null);

  useEffect(() => {
    if (!query.trim()) {
      setResults([]);
      return;
    }

    // 1. Abort any previous pending request
    if (abortRef.current) {
      abortRef.current.abort();
    }

    // 2. Create a new controller for the current query
    const controller = new AbortController();
    abortRef.current = controller;

    setLoading(true);

    fetch(`/api/search?q=${encodeURIComponent(query)}`, {
      signal: controller.signal,
    })
      .then((res) => {
        if (!res.ok) throw new Error("Search failed");
        return res.json();
      })
      .then((data) => {
        setResults(data);
        setLoading(false);
      })
      .catch((err) => {
        // Ignore AbortError — it is intentional!
        if (err.name !== "AbortError") {
          console.error("Search error:", err);
          setLoading(false);
        }
      });

    // Cleanup on unmount
    return () => {
      controller.abort();
    };
  }, [query]);

  return { results, loading };
}
Zero-Dependency Concurrency Limiter (e.g., Max 3 Concurrent Requests)
export function pLimit(concurrency: number) {
  const queue: Array<() => void> = [];
  let activeCount = 0;

  const next = () => {
    activeCount--;
    if (queue.length > 0) {
      const run = queue.shift();
      run?.();
    }
  };

  return function runTask<T>(fn: () => Promise<T>): Promise<T> {
    return new Promise((resolve, reject) => {
      const execute = () => {
        activeCount++;
        fn()
          .then(resolve)
          .catch(reject)
          .finally(next);
      };

      if (activeCount < concurrency) {
        execute();
      } else {
        queue.push(execute);
      }
    });
  };
}

// Usage: Fetch 100 images with max 4 parallel connections
const limit = pLimit(4);
const promises = urls.map((url) => limit(() => fetch(url).then((r) => r.blob())));
const blobs = await Promise.all(promises);

Interview-Ready Answers

Prevent race conditions by aborting prior in-flight requests via AbortController or discarding responses with lower sequence IDs; eliminate duplicate network traffic by caching active in-flight promises.

Follow-up Questions & Deep Dives

🎯 Code Output — What Does This Print?

Question 1hard
const controller = new AbortController();
const signal = controller.signal;

signal.addEventListener("abort", () => console.log("signal aborted"));

console.log("before abort");
controller.abort();
console.log("after abort");
console.log("is aborted:", signal.aborted);
Question 2medium
let latestId = 0;

function handleFetch(query: string) {
  const reqId = ++latestId;
  
  setTimeout(() => {
    if (reqId === latestId) {
      console.log("Applied:", query);
    } else {
      console.log("Discarded stale:", query);
    }
  }, query === "slow" ? 200 : 50);
}

handleFetch("slow");
handleFetch("fast");
// After 250ms, what is printed?

Common Mistakes & Anti-Patterns

  • Catching errors without checking `err.name === 'AbortError'`, causing unwanted error toasts on fast typing
  • Not aborting in-flight requests in `useEffect` cleanup functions, causing state update memory warnings on unmounted components
  • Failing to deduplicate simultaneous GET requests across sibling components
  • Spawning hundreds of unthrottled concurrent requests, saturating the browser network thread pool

Real-World Architectural Scenario

A user rapidly clicks through 10 filter tabs in 3 seconds. The server takes 1.5 seconds per tab query. The UI ends up displaying tab #3's data even though tab #10 is highlighted. How do you guarantee the correct tab data is shown?

Rate Your Readiness

Rate how comfortably you can explain this in an interview to update your global readiness gauge:

Rate your confidence: