Learning Guides
Menu

Async Patterns

11 min readFrontend Patterns & Concepts

Async Patterns

Asynchronous programming is fundamental to JavaScript. Understanding these patterns helps you write efficient, maintainable code that handles complex async scenarios gracefully.

Promise Fundamentals

Creating Promises

JAVASCRIPT
// Basic promise creation
const promise = new Promise((resolve, reject) => {
  // Async operation
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve("Operation completed");
    } else {
      reject(new Error("Operation failed"));
    }
  }, 1000);
});
 
// Promise states: pending → fulfilled or rejected
promise
  .then((result) => console.log(result))
  .catch((error) => console.error(error))
  .finally(() => console.log("Cleanup"));

Promisifying Callbacks

JAVASCRIPT
// Convert callback-based API to Promise
function readFileAsync(path) {
  return new Promise((resolve, reject) => {
    fs.readFile(path, "utf8", (error, data) => {
      if (error) reject(error);
      else resolve(data);
    });
  });
}
 
// Generic promisify utility
function promisify(fn) {
  return function (...args) {
    return new Promise((resolve, reject) => {
      fn(...args, (error, result) => {
        if (error) reject(error);
        else resolve(result);
      });
    });
  };
}
 
const readFile = promisify(fs.readFile);
const data = await readFile("file.txt", "utf8");

Async/Await Patterns

Sequential Execution

JAVASCRIPT
async function processUserData(userId) {
  try {
    // Each await waits for the previous to complete
    const user = await fetchUser(userId);
    const posts = await fetchPosts(user.id);
    const comments = await fetchComments(posts.map((p) => p.id));
 
    return { user, posts, comments };
  } catch (error) {
    console.error("Failed to process user data:", error);
    throw error;
  }
}

Parallel Execution

JAVASCRIPT
async function fetchDashboardData(userId) {
  // All requests start simultaneously
  const [user, notifications, stats] = await Promise.all([
    fetchUser(userId),
    fetchNotifications(userId),
    fetchStats(userId),
  ]);
 
  return { user, notifications, stats };
}
 
// With error handling for independent requests
async function fetchDashboardDataSafe(userId) {
  const results = await Promise.allSettled([
    fetchUser(userId),
    fetchNotifications(userId),
    fetchStats(userId),
  ]);
 
  return {
    user: results[0].status === "fulfilled" ? results[0].value : null,
    notifications: results[1].status === "fulfilled" ? results[1].value : [],
    stats: results[2].status === "fulfilled" ? results[2].value : null,
  };
}

Race Conditions

JAVASCRIPT
// First to complete wins
async function fetchWithTimeout(url, timeout = 5000) {
  const controller = new AbortController();
 
  const fetchPromise = fetch(url, { signal: controller.signal });
 
  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => {
      controller.abort();
      reject(new Error("Request timed out"));
    }, timeout);
  });
 
  return Promise.race([fetchPromise, timeoutPromise]);
}
 
// First successful result (Promise.any)
async function fetchFromMultipleSources(sources) {
  try {
    // Returns first successful response
    return await Promise.any(sources.map((url) => fetch(url)));
  } catch (error) {
    // AggregateError if all fail
    throw new Error("All sources failed");
  }
}

Error Handling Patterns

Try-Catch with Async/Await

JAVASCRIPT
async function fetchData(url) {
  try {
    const response = await fetch(url);
 
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
 
    return await response.json();
  } catch (error) {
    if (error.name === "AbortError") {
      console.log("Request was cancelled");
    } else if (error.name === "TypeError") {
      console.error("Network error or invalid URL");
    } else {
      console.error("Fetch error:", error.message);
    }
    throw error;
  }
}

Error Wrapping Utility

JAVASCRIPT
// Returns [error, result] tuple
async function to(promise) {
  try {
    const result = await promise;
    return [null, result];
  } catch (error) {
    return [error, null];
  }
}
 
// Usage - no try-catch needed
async function processUser(userId) {
  const [userError, user] = await to(fetchUser(userId));
  if (userError) {
    return handleUserError(userError);
  }
 
  const [postsError, posts] = await to(fetchPosts(user.id));
  if (postsError) {
    return handlePostsError(postsError);
  }
 
  return { user, posts };
}

Retry with Exponential Backoff

JAVASCRIPT
async function retryWithBackoff(fn, options = {}) {
  const {
    maxRetries = 3,
    baseDelay = 1000,
    maxDelay = 10000,
    shouldRetry = () => true,
  } = options;
 
  let lastError;
 
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;
 
      if (attempt === maxRetries || !shouldRetry(error)) {
        throw error;
      }
 
      // Exponential backoff with jitter
      const delay = Math.min(
        baseDelay * Math.pow(2, attempt) + Math.random() * 1000,
        maxDelay,
      );
 
      console.log(`Attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
      await sleep(delay);
    }
  }
 
  throw lastError;
}
 
function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}
 
// Usage
const data = await retryWithBackoff(
  () => fetch("/api/data").then((r) => r.json()),
  {
    maxRetries: 3,
    shouldRetry: (error) => error.status !== 404, // Don't retry 404s
  },
);

Request Cancellation

AbortController

JAVASCRIPT
class CancellableRequest {
  constructor() {
    this.controller = null;
  }
 
  async fetch(url, options = {}) {
    // Cancel previous request
    this.cancel();
 
    // Create new controller
    this.controller = new AbortController();
 
    try {
      const response = await fetch(url, {
        ...options,
        signal: this.controller.signal,
      });
 
      return await response.json();
    } catch (error) {
      if (error.name === "AbortError") {
        return null; // Cancelled, not an error
      }
      throw error;
    }
  }
 
  cancel() {
    if (this.controller) {
      this.controller.abort();
      this.controller = null;
    }
  }
}
 
// Usage in search
const searchRequest = new CancellableRequest();
 
async function handleSearchInput(query) {
  // Previous search is automatically cancelled
  const results = await searchRequest.fetch(`/api/search?q=${query}`);
  if (results) {
    displayResults(results);
  }
}
 
// In React
function useSearch() {
  const [results, setResults] = useState([]);
  const controllerRef = useRef(null);
 
  const search = useCallback(async (query) => {
    // Cancel previous
    controllerRef.current?.abort();
    controllerRef.current = new AbortController();
 
    try {
      const response = await fetch(`/api/search?q=${query}`, {
        signal: controllerRef.current.signal,
      });
      const data = await response.json();
      setResults(data);
    } catch (error) {
      if (error.name !== "AbortError") {
        console.error(error);
      }
    }
  }, []);
 
  // Cleanup on unmount
  useEffect(() => {
    return () => controllerRef.current?.abort();
  }, []);
 
  return { results, search };
}

Concurrent Control Patterns

Rate Limiting

JAVASCRIPT
class RateLimiter {
  constructor(requestsPerSecond) {
    this.requestsPerSecond = requestsPerSecond;
    this.tokens = requestsPerSecond;
    this.lastRefill = Date.now();
    this.queue = [];
  }
 
  async acquire() {
    this.refill();
 
    if (this.tokens > 0) {
      this.tokens--;
      return Promise.resolve();
    }
 
    // Wait for token
    return new Promise((resolve) => {
      this.queue.push(resolve);
      setTimeout(() => this.processQueue(), 1000 / this.requestsPerSecond);
    });
  }
 
  refill() {
    const now = Date.now();
    const elapsed = (now - this.lastRefill) / 1000;
    this.tokens = Math.min(
      this.requestsPerSecond,
      this.tokens + elapsed * this.requestsPerSecond,
    );
    this.lastRefill = now;
  }
 
  processQueue() {
    this.refill();
    while (this.tokens > 0 && this.queue.length > 0) {
      this.tokens--;
      const resolve = this.queue.shift();
      resolve();
    }
  }
}
 
// Usage
const limiter = new RateLimiter(10); // 10 requests per second
 
async function rateLimitedFetch(url) {
  await limiter.acquire();
  return fetch(url);
}

Semaphore (Concurrency Limit)

JAVASCRIPT
class Semaphore {
  constructor(maxConcurrent) {
    this.maxConcurrent = maxConcurrent;
    this.current = 0;
    this.queue = [];
  }
 
  async acquire() {
    if (this.current < this.maxConcurrent) {
      this.current++;
      return Promise.resolve();
    }
 
    return new Promise((resolve) => {
      this.queue.push(resolve);
    });
  }
 
  release() {
    this.current--;
    if (this.queue.length > 0) {
      this.current++;
      const resolve = this.queue.shift();
      resolve();
    }
  }
 
  async run(fn) {
    await this.acquire();
    try {
      return await fn();
    } finally {
      this.release();
    }
  }
}
 
// Process items with max 5 concurrent operations
async function processWithLimit(items, processor, limit = 5) {
  const semaphore = new Semaphore(limit);
 
  return Promise.all(items.map((item) => semaphore.run(() => processor(item))));
}
 
// Usage
const urls = ["url1", "url2" /* ...100 more */];
const results = await processWithLimit(
  urls,
  (url) => fetch(url).then((r) => r.json()),
  5, // Max 5 concurrent requests
);

Batching Requests

JAVASCRIPT
class RequestBatcher {
  constructor(batchFn, options = {}) {
    this.batchFn = batchFn;
    this.maxBatchSize = options.maxBatchSize || 10;
    this.maxWaitMs = options.maxWaitMs || 50;
    this.queue = [];
    this.timeout = null;
  }
 
  async request(item) {
    return new Promise((resolve, reject) => {
      this.queue.push({ item, resolve, reject });
 
      if (this.queue.length >= this.maxBatchSize) {
        this.flush();
      } else if (!this.timeout) {
        this.timeout = setTimeout(() => this.flush(), this.maxWaitMs);
      }
    });
  }
 
  async flush() {
    if (this.timeout) {
      clearTimeout(this.timeout);
      this.timeout = null;
    }
 
    if (this.queue.length === 0) return;
 
    const batch = this.queue.splice(0, this.maxBatchSize);
    const items = batch.map((b) => b.item);
 
    try {
      const results = await this.batchFn(items);
      batch.forEach((b, i) => b.resolve(results[i]));
    } catch (error) {
      batch.forEach((b) => b.reject(error));
    }
  }
}
 
// Usage: batch user fetches
const userBatcher = new RequestBatcher(
  async (userIds) => {
    // Single request for multiple users
    const response = await fetch("/api/users", {
      method: "POST",
      body: JSON.stringify({ ids: userIds }),
    });
    return response.json();
  },
  { maxBatchSize: 50, maxWaitMs: 10 },
);
 
// These get batched into one request
const [user1, user2, user3] = await Promise.all([
  userBatcher.request("id1"),
  userBatcher.request("id2"),
  userBatcher.request("id3"),
]);

Async Iteration

AsyncIterator for Pagination

JAVASCRIPT
async function* fetchAllPages(baseUrl) {
  let page = 1;
  let hasMore = true;
 
  while (hasMore) {
    const response = await fetch(`${baseUrl}?page=${page}`);
    const data = await response.json();
 
    for (const item of data.items) {
      yield item;
    }
 
    hasMore = data.hasNextPage;
    page++;
  }
}
 
// Usage with for-await
async function processAllItems(url) {
  for await (const item of fetchAllPages(url)) {
    await processItem(item);
  }
}
 
// Collect all items
async function getAllItems(url) {
  const items = [];
  for await (const item of fetchAllPages(url)) {
    items.push(item);
  }
  return items;
}

Streaming with AsyncIterator

JAVASCRIPT
async function* streamResponse(response) {
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
 
  try {
    while (true) {
      const { done, value } = await reader.read();
 
      if (done) break;
 
      yield decoder.decode(value, { stream: true });
    }
  } finally {
    reader.releaseLock();
  }
}
 
// Process streaming response
async function processStream(url) {
  const response = await fetch(url);
  let buffer = "";
 
  for await (const chunk of streamResponse(response)) {
    buffer += chunk;
 
    // Process complete lines
    const lines = buffer.split("\n");
    buffer = lines.pop(); // Keep incomplete line
 
    for (const line of lines) {
      if (line.trim()) {
        const data = JSON.parse(line);
        handleData(data);
      }
    }
  }
}

Event-Driven Async

Promise-based Event Handling

JAVASCRIPT
function once(emitter, event) {
  return new Promise((resolve, reject) => {
    const handleSuccess = (data) => {
      emitter.off("error", handleError);
      resolve(data);
    };
 
    const handleError = (error) => {
      emitter.off(event, handleSuccess);
      reject(error);
    };
 
    emitter.once(event, handleSuccess);
    emitter.once("error", handleError);
  });
}
 
// Usage
const data = await once(socket, "message");
 
// With timeout
async function onceWithTimeout(emitter, event, timeout) {
  return Promise.race([
    once(emitter, event),
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error("Timeout")), timeout),
    ),
  ]);
}

Async Queue

JAVASCRIPT
class AsyncQueue {
  constructor() {
    this.queue = [];
    this.waiters = [];
  }
 
  enqueue(item) {
    if (this.waiters.length > 0) {
      const waiter = this.waiters.shift();
      waiter.resolve(item);
    } else {
      this.queue.push(item);
    }
  }
 
  async dequeue() {
    if (this.queue.length > 0) {
      return this.queue.shift();
    }
 
    return new Promise((resolve) => {
      this.waiters.push({ resolve });
    });
  }
 
  async *[Symbol.asyncIterator]() {
    while (true) {
      yield await this.dequeue();
    }
  }
}
 
// Producer-consumer pattern
const queue = new AsyncQueue();
 
// Producer
async function producer() {
  for (let i = 0; i < 10; i++) {
    await sleep(100);
    queue.enqueue({ id: i, data: `Item ${i}` });
  }
}
 
// Consumer
async function consumer() {
  for await (const item of queue) {
    console.log("Processing:", item);
    // Process item
  }
}

React Async Patterns

useAsync Hook

JAVASCRIPT
function useAsync(asyncFn, deps = []) {
  const [state, setState] = useState({
    data: null,
    loading: true,
    error: null,
  });
 
  const execute = useCallback(async () => {
    setState((s) => ({ ...s, loading: true, error: null }));
 
    try {
      const data = await asyncFn();
      setState({ data, loading: false, error: null });
      return data;
    } catch (error) {
      setState({ data: null, loading: false, error });
      throw error;
    }
  }, deps);
 
  useEffect(() => {
    execute();
  }, [execute]);
 
  return { ...state, execute };
}
 
// Usage
function UserProfile({ userId }) {
  const {
    data: user,
    loading,
    error,
    execute: refetch,
  } = useAsync(() => fetchUser(userId), [userId]);
 
  if (loading) return <Spinner />;
  if (error) return <Error error={error} onRetry={refetch} />;
  return <Profile user={user} />;
}

Suspense for Data Fetching

JAVASCRIPT
// Resource wrapper for Suspense
function wrapPromise(promise) {
  let status = "pending";
  let result;
 
  const suspender = promise.then(
    (r) => {
      status = "success";
      result = r;
    },
    (e) => {
      status = "error";
      result = e;
    },
  );
 
  return {
    read() {
      if (status === "pending") throw suspender;
      if (status === "error") throw result;
      return result;
    },
  };
}
 
// Create resource
const userResource = wrapPromise(fetchUser(userId));
 
// Component reads synchronously
function UserProfile() {
  const user = userResource.read();
  return <Profile user={user} />;
}
 
// Usage with Suspense
<Suspense fallback={<Spinner />}>
  <UserProfile />
</Suspense>;

Summary

PatternUse CaseKey Benefit
Promise.allParallel independent tasksMaximum parallelism
Promise.allSettledParallel with fault toleranceNo short-circuit on error
Promise.raceTimeout, fastest sourceFirst result wins
Promise.anyRedundant sourcesFirst success wins
AbortControllerCancellationClean resource cleanup
Retry with backoffTransient failuresResilient requests
SemaphoreConcurrency limitPrevent overload
BatchingMany small requestsReduce overhead
AsyncIteratorPagination, streamsMemory efficient

Key Principles:

  1. Handle errors at appropriate levels
  2. Always consider cancellation
  3. Limit concurrency to prevent resource exhaustion
  4. Use timeouts for external calls
  5. Clean up resources in finally blocks