JavaScript

Async/Await

Master asynchronous JavaScript with Promises and async/await for clean, readable async code.

Understanding Promises

A Promise represents the eventual result of an asynchronous operation. It can be in one of three states: pending, fulfilled, or rejected.

// Creating a Promise
const fetchData = new Promise((resolve, reject) => {
  const success = Math.random() > 0.5;

  setTimeout(() => {
    if (success) {
      resolve({ data: "Hello!", status: 200 });
    } else {
      reject(new Error("Network error"));
    }
  }, 1000);
});

// Consuming a Promise with .then/.catch
fetchData
  .then(result => {
    console.log("Success:", result.data);
    return result.status; // Pass value to next .then
  })
  .then(status => {
    console.log("Status:", status);
  })
  .catch(error => {
    console.error("Error:", error.message);
  })
  .finally(() => {
    console.log("Always runs");
  });

// Promise.all - wait for multiple promises
const promises = [
  fetch('/api/users'),
  fetch('/api/posts'),
  fetch('/api/comments'),
];

Promise.all(promises)
  .then(responses => Promise.all(responses.map(r => r.json())))
  .then(([users, posts, comments]) => {
    console.log({ users, posts, comments });
  });

Async/Await Syntax

Async/await is syntactic sugar over Promises that makes asynchronous code look and behave more like synchronous code. It's much easier to read and debug.

// async function always returns a Promise
async function fetchUser(id) {
  // await pauses execution until Promise resolves
  const response = await fetch(`/api/users/${id}`);

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  const user = await response.json();
  return user;
}

// Error handling with try/catch
async function getUserPosts(userId) {
  try {
    const user = await fetchUser(userId);
    const postsResponse = await fetch(`/api/posts?userId=${userId}`);
    const posts = await postsResponse.json();

    return { user, posts };
  } catch (error) {
    console.error("Failed to fetch:", error.message);
    return null;
  }
}

// Running multiple async operations in parallel
async function loadDashboard() {
  // Sequential (slow) - don't do this
  // const users = await fetchUsers();
  // const stats = await fetchStats();

  // Parallel (fast) - do this instead
  const [users, stats] = await Promise.all([
    fetchUsers(),
    fetchStats(),
  ]);

  return { users, stats };
}

// Top-level await (in modules)
const config = await fetch('/api/config').then(r => r.json());

Error Handling Patterns

Proper error handling in async code prevents silent failures and improves user experience. Here are proven patterns for robust error handling.

// Pattern 1: Try/catch
async function withTryCatch() {
  try {
    const data = await riskyOperation();
    return { data, error: null };
  } catch (error) {
    return { data: null, error };
  }
}

// Pattern 2: Result tuple (inspired by Go)
async function safeAsync(promise) {
  try {
    const data = await promise;
    return [null, data];
  } catch (error) {
    return [error, null];
  }
}

// Usage of Result tuple
async function example() {
  const [error, data] = await safeAsync(fetchUser(1));

  if (error) {
    console.error("Failed:", error.message);
    return;
  }

  console.log("User:", data);
}

// Pattern 3: AbortController for cancellable requests
async function fetchWithTimeout(url, timeoutMs = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const response = await fetch(url, { signal: controller.signal });
    clearTimeout(timeoutId);
    return await response.json();
  } catch (error) {
    if (error.name === 'AbortError') {
      throw new Error(`Request timed out after ${timeoutMs}ms`);
    }
    throw error;
  }
}

Always handle Promise rejections. Unhandled rejections can crash Node.js applications and cause silent failures in browsers.