DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Structured Concurrency in JavaScript: Beyond Promise.all

The Problem with Unstructured Async Code

JavaScript async code has a scope problem. You fire off promises and hope they complete—or fail—cleanly. When something goes wrong mid-flight, cleanup is your responsibility.

// Classic problem: partial failure
async function loadDashboard(userId: string) {
  const [user, orders, analytics] = await Promise.all([
    getUser(userId),
    getOrders(userId),     // This throws after 2 seconds
    getAnalytics(userId),  // This is still running!
  ]);
  // getAnalytics never gets cancelled
}
Enter fullscreen mode Exit fullscreen mode

When getOrders rejects, Promise.all rejects—but getAnalytics keeps running in the background, consuming resources, potentially writing stale data.

Promise.allSettled: Handle All Results

async function loadDashboard(userId: string) {
  const results = await Promise.allSettled([
    getUser(userId),
    getOrders(userId),
    getAnalytics(userId),
  ]);

  const [userResult, ordersResult, analyticsResult] = results;

  // Handle each independently
  const user = userResult.status === 'fulfilled' ? userResult.value : null;
  const orders = ordersResult.status === 'fulfilled' ? ordersResult.value : [];

  if (analyticsResult.status === 'rejected') {
    console.error('Analytics failed:', analyticsResult.reason);
  }

  return { user, orders, analytics: analyticsResult.status === 'fulfilled' ? analyticsResult.value : null };
}
Enter fullscreen mode Exit fullscreen mode

AbortController: Actual Cancellation

async function fetchWithTimeout<T>(url: string, timeoutMs: number): Promise<T> {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const response = await fetch(url, { signal: controller.signal });
    return response.json();
  } finally {
    clearTimeout(timeoutId);
  }
}

// Cancel when component unmounts (React)
function useUserData(userId: string) {
  const [data, setData] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    fetch(`/api/users/${userId}`, { signal: controller.signal })
      .then(r => r.json())
      .then(setData)
      .catch(err => {
        if (err.name !== 'AbortError') throw err;
        // AbortError is expected on cleanup, ignore it
      });

    return () => controller.abort(); // cleanup
  }, [userId]);

  return data;
}
Enter fullscreen mode Exit fullscreen mode

Promise.race: First Wins

// Timeout pattern
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
  const timeout = new Promise<never>((_, reject) =>
    setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
  );
  return Promise.race([promise, timeout]);
}

// Fastest cache or network
async function getWithFallback(key: string) {
  return Promise.race([
    redis.get(key).then(v => JSON.parse(v!)), // cache
    db.slowQuery(key),                          // database
  ]);
}
Enter fullscreen mode Exit fullscreen mode

Promise.any: First Success

// Try multiple mirrors, use whichever responds first
async function fetchFromCDN(path: string) {
  return Promise.any([
    fetch(`https://cdn1.example.com${path}`),
    fetch(`https://cdn2.example.com${path}`),
    fetch(`https://cdn3.example.com${path}`),
  ]);
  // Rejects only if ALL fail (AggregateError)
}
Enter fullscreen mode Exit fullscreen mode

Controlled Concurrency

Running 1000 tasks in parallel overwhelms databases and APIs:

async function processInBatches<T, R>(
  items: T[],
  processor: (item: T) => Promise<R>,
  concurrency: number
): Promise<R[]> {
  const results: R[] = [];

  for (let i = 0; i < items.length; i += concurrency) {
    const batch = items.slice(i, i + concurrency);
    const batchResults = await Promise.all(batch.map(processor));
    results.push(...batchResults);
  }

  return results;
}

// Process 1000 users, 10 at a time
const results = await processInBatches(users, processUser, 10);
Enter fullscreen mode Exit fullscreen mode

Or use p-limit for a semaphore pattern:

import pLimit from 'p-limit';

const limit = pLimit(10); // max 10 concurrent

const results = await Promise.all(
  users.map(user => limit(() => processUser(user)))
);
// All 1000 tasks are queued, but only 10 run at once
Enter fullscreen mode Exit fullscreen mode

Async Iteration

async function* generateUsers(): AsyncGenerator<User> {
  let page = 1;
  while (true) {
    const users = await db.users.findMany({ skip: (page - 1) * 100, take: 100 });
    if (users.length === 0) return;
    yield* users;
    page++;
  }
}

// Process without loading all into memory
for await (const user of generateUsers()) {
  await sendEmail(user.email);
}
Enter fullscreen mode Exit fullscreen mode

The right concurrency primitive depends on your use case:

  • Promise.all — all must succeed
  • Promise.allSettled — handle each result individually
  • Promise.race — first finishes wins
  • Promise.any — first succeeds wins
  • AbortController — actually cancel in-flight requests
  • p-limit — controlled parallelism

Async patterns, concurrency utilities, and error handling built in: Whoff Agents AI SaaS Starter Kit.

Top comments (0)