DNA⚡ JavaScriptAsync Patterns & Concurrency Control
ðŸĶ–DinosaurJavaScriptAsyncArchitecturePatterns

Async Patterns & Concurrency Control

Promises and async/await are just the beginning. Production code needs retry logic, cancellation, rate limiting, and structured concurrency. This is where senior engineers live.

Async Patterns & Concurrency Control

Senior interviews don't ask "what is a Promise." They test whether you can implement retry with backoff, cancel in-flight requests, control concurrency, and handle race conditions. This is production-grade async engineering.

Promise Internals: The State Machine

         ┌─── resolve(value) ───→ Fulfilled (value)
Pending ─â”Ī
         └─── reject(reason) ──→ Rejected (reason)

Once settled, a Promise never changes state. This immutability is what makes Promises composable.

const p = new Promise((resolve, reject) => {
  resolve('first');
  resolve('second');  // Ignored — already fulfilled
  reject('error');    // Ignored — already fulfilled
});

Concurrency Combinators

CombinatorResolves whenRejects whenUse case
Promise.allAll fulfillAny rejectsParallel independent fetches
Promise.allSettledAll settle (either way)Never rejectsBatch ops with partial failure
Promise.raceFirst settlesFirst settles (if rejection)Timeouts, fastest response
Promise.anyFirst fulfillsAll reject (AggregateError)Fallback chains, redundancy

Promise.allSettled — The Production Choice

const results = await Promise.allSettled([
  fetchUser(1),
  fetchUser(2),
  fetchUser(3),  // This might fail
]);
 
const succeeded = results.filter(r => r.status === 'fulfilled').map(r => r.value);
const failed = results.filter(r => r.status === 'rejected').map(r => r.reason);
 
// Process what succeeded, report what failed — don't throw away partial results

Timeout Pattern

function withTimeout(promise, ms, message = 'Operation timed out') {
  let timeoutId;
  const timeout = new Promise((_, reject) => {
    timeoutId = setTimeout(() => reject(new Error(message)), ms);
  });
 
  return Promise.race([promise, timeout]).finally(() => clearTimeout(timeoutId));
}
 
const data = await withTimeout(fetch('/api/slow'), 5000);

AbortController — Proper Cancellation

AbortController is the standard cancellation mechanism for fetch and other async operations:

const controller = new AbortController();
 
fetch('/api/data', { signal: controller.signal })
  .then(res => res.json())
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('Request cancelled');
    } else {
      throw err;
    }
  });
 
controller.abort(); // Cancels the request

React Pattern: Cancel on Unmount/Re-render

function useData(url) {
  const [data, setData] = useState(null);
 
  useEffect(() => {
    const controller = new AbortController();
 
    fetch(url, { signal: controller.signal })
      .then(res => res.json())
      .then(setData)
      .catch(err => {
        if (err.name !== 'AbortError') throw err;
      });
 
    return () => controller.abort();
  }, [url]);
 
  return data;
}

Timeout + Cancellation Combined

async function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
 
  try {
    const response = await fetch(url, {
      ...options,
      signal: controller.signal,
    });
    return response;
  } finally {
    clearTimeout(timeoutId);
  }
}

Retry with Exponential Backoff

async function retry(fn, options = {}) {
  const { attempts = 3, baseDelay = 1000, maxDelay = 30000, shouldRetry = () => true } = options;
 
  for (let attempt = 0; attempt < attempts; attempt++) {
    try {
      return await fn(attempt);
    } catch (error) {
      const isLastAttempt = attempt === attempts - 1;
      if (isLastAttempt || !shouldRetry(error, attempt)) throw error;
 
      const delay = Math.min(baseDelay * 2 ** attempt, maxDelay);
      const jitter = delay * 0.1 * Math.random();
      await new Promise(r => setTimeout(r, delay + jitter));
    }
  }
}
 
// Usage
const data = await retry(
  () => fetch('/api/flaky').then(r => {
    if (!r.ok) throw new Error(`HTTP ${r.status}`);
    return r.json();
  }),
  {
    attempts: 3,
    baseDelay: 1000,
    shouldRetry: (err) => !err.message.includes('401'),
  }
);

Jitter is critical — without it, all clients retry at the same time after an outage, creating a "thundering herd."

Concurrency Limiting

Promise.all fires everything at once. When you have 1000 items and a rate-limited API, you need controlled concurrency:

async function mapConcurrent(items, fn, concurrency = 5) {
  const results = new Array(items.length);
  const executing = new Set();
 
  for (let i = 0; i < items.length; i++) {
    const promise = fn(items[i], i).then(result => {
      results[i] = result;
      executing.delete(promise);
    });
 
    executing.add(promise);
 
    if (executing.size >= concurrency) {
      await Promise.race(executing);
    }
  }
 
  await Promise.all(executing);
  return results;
}
 
// Process 1000 images, 5 at a time
const thumbnails = await mapConcurrent(
  images,
  (img) => generateThumbnail(img),
  5
);

Sequential vs Parallel Execution

// ❌ Sequential (2x slower) — each awaits before starting next
const user = await fetchUser(id);
const posts = await fetchPosts(id);
 
// ✅ Parallel — both start immediately
const [user, posts] = await Promise.all([
  fetchUser(id),
  fetchPosts(id),
]);
 
// ✅ Parallel with independent error handling
const [userResult, postsResult] = await Promise.allSettled([
  fetchUser(id),
  fetchPosts(id),
]);

The Waterfall Anti-Pattern

// ❌ Serial waterfall — each query waits for the previous
const users = await fetchUsers();
const posts = await fetchPosts();
const comments = await fetchComments();
 
// ✅ Parallel when independent
const [users, posts, comments] = await Promise.all([
  fetchUsers(),
  fetchPosts(),
  fetchComments(),
]);
 
// ✅ Waterfall when dependent (this is correct)
const user = await fetchUser(id);
const posts = await fetchPostsByAuthor(user.authorId);

Error Handling Patterns

The Result Pattern (Rust/Go-style)

async function safeAsync(fn) {
  try {
    return [await fn(), null];
  } catch (error) {
    return [null, error];
  }
}
 
const [user, error] = await safeAsync(() => fetchUser(1));
if (error) {
  logger.error('Failed to fetch user', error);
  return fallbackUser;
}

Structured Error Types

class AppError extends Error {
  constructor(message, code, cause) {
    super(message);
    this.code = code;
    this.cause = cause;
  }
}
 
class NetworkError extends AppError {
  constructor(message, statusCode, cause) {
    super(message, 'NETWORK_ERROR', cause);
    this.statusCode = statusCode;
  }
 
  get isRetryable() {
    return this.statusCode >= 500 || this.statusCode === 429;
  }
}
 
async function fetchJSON(url) {
  try {
    const res = await fetch(url);
    if (!res.ok) throw new NetworkError(`HTTP ${res.status}`, res.status);
    return res.json();
  } catch (err) {
    if (err instanceof NetworkError) throw err;
    throw new NetworkError('Network failure', 0, err);
  }
}

Async Generators & Iteration

For streaming or paginated data:

async function* paginate(fetchPage) {
  let page = 1;
  let hasMore = true;
 
  while (hasMore) {
    const result = await fetchPage(page);
    yield* result.items;
    hasMore = result.hasMore;
    page++;
  }
}
 
// Usage
for await (const user of paginate((p) => fetchUsers({ page: p }))) {
  processUser(user);
}

Debounced Async (Search Pattern)

function createDebouncedFetcher(fetchFn, delay = 300) {
  let timeoutId = null;
  let controller = null;
 
  return async function(query) {
    controller?.abort();
    clearTimeout(timeoutId);
 
    return new Promise((resolve, reject) => {
      timeoutId = setTimeout(async () => {
        controller = new AbortController();
        try {
          const result = await fetchFn(query, controller.signal);
          resolve(result);
        } catch (err) {
          if (err.name !== 'AbortError') reject(err);
        }
      }, delay);
    });
  };
}

Interview Signal

Senior candidates demonstrate:

  1. Combinator mastery — Choosing the right Promise combinator for the scenario
  2. Production patterns — Retry, timeout, cancellation, concurrency limiting
  3. Race condition awareness — AbortController for stale requests, latest-only patterns
  4. Error architecture — Structured errors, retryable vs terminal failures
  5. Performance thinking — Parallel vs sequential, avoiding waterfalls, controlled concurrency