Skip to content

Promises & Async/Await

Modern patterns for handling asynchronous JavaScript.

Promises Basics

A Promise represents a value that may be available now, later, or never.

// Creating a Promise
const promise = new Promise((resolve, reject) => {
  // Async operation
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve('Data loaded');
    } else {
      reject(new Error('Failed to load'));
    }
  }, 1000);
});

// Using the Promise
promise
  .then(data => console.log(data))
  .catch(error => console.error(error));

Promise States

Pending ──────┬────────▶ Fulfilled (resolved)
              └────────▶ Rejected

Once settled (fulfilled or rejected), a promise cannot change state.

Promise Methods

Promise.resolve() and Promise.reject()

// Create immediately resolved promise
const resolved = Promise.resolve('value');

// Create immediately rejected promise
const rejected = Promise.reject(new Error('error'));

// Useful for wrapping values
function maybeAsync(value) {
  if (cached.has(value)) {
    return Promise.resolve(cached.get(value));
  }
  return fetchFromServer(value);
}

Promise.all()

Wait for all promises, fail if any fails.

async function fetchUserData(userId) {
  const [user, posts, comments] = await Promise.all([
    fetch(`/users/${userId}`).then(r => r.json()),
    fetch(`/posts?userId=${userId}`).then(r => r.json()),
    fetch(`/comments?userId=${userId}`).then(r => r.json()),
  ]);

  return { user, posts, comments };
}

// If any promise rejects, the whole thing rejects
try {
  const data = await Promise.all([
    Promise.resolve(1),
    Promise.reject(new Error('Oops')),
    Promise.resolve(3),
  ]);
} catch (error) {
  console.error(error); // Error: Oops
}

Promise.allSettled()

Wait for all, get results regardless of success/failure.

const results = await Promise.allSettled([
  fetch('/api/users'),
  fetch('/api/posts'),
  fetch('/api/broken'),  // This fails
]);

results.forEach((result, index) => {
  if (result.status === 'fulfilled') {
    console.log(`Request ${index} succeeded:`, result.value);
  } else {
    console.log(`Request ${index} failed:`, result.reason);
  }
});

// Output:
// Request 0 succeeded: Response {...}
// Request 1 succeeded: Response {...}
// Request 2 failed: Error: ...

Promise.race()

Resolve/reject with the first settled promise.

// Timeout pattern
async function fetchWithTimeout(url, timeout = 5000) {
  const fetchPromise = fetch(url);
  const timeoutPromise = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), timeout)
  );

  return Promise.race([fetchPromise, timeoutPromise]);
}

Promise.any()

Resolve with the first fulfilled promise (ignores rejections).

// Try multiple servers, use first successful
const result = await Promise.any([
  fetch('https://server1.com/data'),
  fetch('https://server2.com/data'),
  fetch('https://server3.com/data'),
]);

// Only rejects if ALL promises reject

Async/Await

Syntactic sugar over Promises for cleaner code.

Basic Usage

// Without async/await
function fetchUser(id) {
  return fetch(`/users/${id}`)
    .then(response => response.json())
    .then(user => {
      return fetch(`/posts?userId=${user.id}`);
    })
    .then(response => response.json());
}

// With async/await
async function fetchUser(id) {
  const response = await fetch(`/users/${id}`);
  const user = await response.json();

  const postsResponse = await fetch(`/posts?userId=${user.id}`);
  const posts = await postsResponse.json();

  return { user, posts };
}

Error Handling

// try/catch works with await
async function fetchData() {
  try {
    const response = await fetch('/api/data');
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    console.error('Failed to fetch:', error);
    throw error; // Re-throw if needed
  }
}

// Or handle at call site
const data = await fetchData().catch(error => {
  console.error(error);
  return null;
});

Parallel vs Sequential

// Sequential (slower)
async function sequential() {
  const user = await fetchUser();      // Wait...
  const posts = await fetchPosts();    // Then wait...
  const comments = await fetchComments(); // Then wait...
  return { user, posts, comments };
}

// Parallel (faster)
async function parallel() {
  const [user, posts, comments] = await Promise.all([
    fetchUser(),
    fetchPosts(),
    fetchComments(),
  ]);
  return { user, posts, comments };
}

Conditional Parallel

async function fetchUserData(userId, options = {}) {
  const promises = [fetchUser(userId)];

  if (options.includePosts) {
    promises.push(fetchPosts(userId));
  }
  if (options.includeComments) {
    promises.push(fetchComments(userId));
  }

  const results = await Promise.all(promises);

  return {
    user: results[0],
    posts: options.includePosts ? results[1] : [],
    comments: options.includeComments ? results[options.includePosts ? 2 : 1] : [],
  };
}

Common Patterns

Retry with Backoff

async function fetchWithRetry(url, options = {}) {
  const { retries = 3, backoff = 1000 } = options;

  for (let attempt = 0; attempt < retries; attempt++) {
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return await response.json();
    } catch (error) {
      if (attempt === retries - 1) throw error;

      const delay = backoff * Math.pow(2, attempt);
      await new Promise(r => setTimeout(r, delay));
    }
  }
}

Timeout Wrapper

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

// Usage
const data = await withTimeout(fetch('/api/slow'), 5000);

Rate Limiting

class RateLimiter {
  private queue: (() => void)[] = [];
  private running = 0;

  constructor(private maxConcurrent: number) {}

  async add<T>(fn: () => Promise<T>): Promise<T> {
    if (this.running >= this.maxConcurrent) {
      await new Promise<void>(resolve => this.queue.push(resolve));
    }

    this.running++;
    try {
      return await fn();
    } finally {
      this.running--;
      const next = this.queue.shift();
      if (next) next();
    }
  }
}

// Usage
const limiter = new RateLimiter(5);
const results = await Promise.all(
  urls.map(url => limiter.add(() => fetch(url)))
);

Batching

async function processBatch<T, R>(
  items: T[],
  batchSize: number,
  fn: (item: T) => Promise<R>
): Promise<R[]> {
  const results: R[] = [];

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

  return results;
}

// Process 100 items, 10 at a time
const results = await processBatch(items, 10, processItem);

Sequential Processing

// When order matters
async function processSequentially<T, R>(
  items: T[],
  fn: (item: T) => Promise<R>
): Promise<R[]> {
  const results: R[] = [];

  for (const item of items) {
    const result = await fn(item);
    results.push(result);
  }

  return results;
}

// Or with reduce
async function processSequentially2<T, R>(
  items: T[],
  fn: (item: T) => Promise<R>
): Promise<R[]> {
  return items.reduce(async (accPromise, item) => {
    const acc = await accPromise;
    const result = await fn(item);
    return [...acc, result];
  }, Promise.resolve([] as R[]));
}

Caching

const cache = new Map<string, Promise<Response>>();

async function fetchCached(url: string): Promise<Response> {
  if (!cache.has(url)) {
    // Store the promise, not the result
    cache.set(url, fetch(url));
  }

  try {
    return await cache.get(url)!.then(r => r.clone());
  } catch (error) {
    cache.delete(url); // Remove failed requests
    throw error;
  }
}

Error Handling Patterns

Error Types

class NetworkError extends Error {
  constructor(message: string, public status?: number) {
    super(message);
    this.name = 'NetworkError';
  }
}

async function fetchWithErrorTypes(url: string) {
  try {
    const response = await fetch(url);

    if (!response.ok) {
      throw new NetworkError(`HTTP error`, response.status);
    }

    return await response.json();
  } catch (error) {
    if (error instanceof NetworkError) {
      // Handle network errors
      console.error('Network error:', error.status);
    } else if (error instanceof SyntaxError) {
      // JSON parse error
      console.error('Invalid JSON');
    } else {
      throw error;
    }
  }
}

Graceful Degradation

async function fetchWithFallback<T>(
  primary: () => Promise<T>,
  fallback: () => Promise<T>
): Promise<T> {
  try {
    return await primary();
  } catch (error) {
    console.warn('Primary failed, using fallback:', error);
    return await fallback();
  }
}

// Usage
const data = await fetchWithFallback(
  () => fetch('/api/v2/data').then(r => r.json()),
  () => fetch('/api/v1/data').then(r => r.json())
);

Common Mistakes

// Bad: forEach doesn't wait for async
items.forEach(async item => {
  await processItem(item);  // Doesn't actually wait!
});

// Good: Use for...of
for (const item of items) {
  await processItem(item);
}

// Or Promise.all for parallel
await Promise.all(items.map(item => processItem(item)));

// Bad: Catching too broadly
try {
  const data = await riskyOperation();
  processData(data); // Errors here are also caught!
} catch (error) {
  // Can't tell which operation failed
}

// Good: Narrow try/catch
let data;
try {
  data = await riskyOperation();
} catch (error) {
  throw new Error('Failed to fetch data');
}
processData(data);

// Bad: Not awaiting in return
async function bad() {
  return fetch('/api/data').json();  // Missing await!
}

// Good
async function good() {
  const response = await fetch('/api/data');
  return response.json();  // Still need to handle this promise
}