Async Error Handling

Things go wrong. Networks fail. APIs break. Handle it gracefully.

Promise Errors

Rejections and .catch() — the Promise way to handle errors.

// A Promise can reject:
const promise = new Promise((resolve, reject) => {
  reject(new Error("Something went wrong"));
});

// Or throw inside an executor:
const promise2 = new Promise(() => {
  throw new Error("Oops"); // automatically rejects
});

Promises reject when: you call reject(), an error is thrown inside the executor, or a .then() callback throws.

1 / 3

try/catch + async

The cleanest way to handle async errors.

async function loadUserData(userId) {
  try {
    const res = await fetch(`/api/users/${userId}`);
    
    // Network succeeded but HTTP error:
    if (!res.ok) {
      throw new Error(`HTTP ${res.status}: ${res.statusText}`);
    }
    
    const user = await res.json();
    return user;
    
  } catch (error) {
    // Catches: network errors, HTTP errors, JSON errors
    if (error.name === "TypeError") {
      console.error("Network error:", error.message);
    } else {
      console.error("Request failed:", error.message);
    }
    return null;
    
  } finally {
    // Always runs — success or failure:
    hideLoadingSpinner();
  }
}

try

happy path

catch

error path

finally

always runs

Error Propagation

Errors bubble up through async call chains.

// Errors propagate UP the call chain:
async function getUser(id) {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error("User not found");
  return res.json();
}

async function getUserPosts(userId) {
  const user = await getUser(userId); // can throw!
  const res = await fetch(`/api/posts/${user.id}`);
  return res.json();
}

async function displayDashboard(userId) {
  try {
    const posts = await getUserPosts(userId);
    render(posts);
  } catch (error) {
    // Catches errors from getUser OR getUserPosts
    showError(error.message);
  }
}

💡 You don't need try/catch in every function. Let errors propagate and catch them at the level where you can handle them meaningfully (show UI, retry, fallback).

Recovery Patterns

Graceful degradation when things fail.

// Pattern 1: Fallback value
async function getConfig() {
  try {
    const res = await fetch("/api/config");
    return await res.json();
  } catch {
    return { theme: "light", lang: "en" }; // defaults
  }
}

// Pattern 2: Retry logic
async function fetchWithRetry(url, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    try {
      const res = await fetch(url);
      if (res.ok) return await res.json();
      throw new Error(`HTTP ${res.status}`);
    } catch (err) {
      if (i === attempts - 1) throw err; // give up
      await delay(1000 * 2 ** i); // exponential backoff
    }
  }
}

// Pattern 3: Partial failure (load what you can)
async function loadDashboard() {
  const [users, posts, stats] = await Promise.allSettled([
    fetchUsers(),
    fetchPosts(),
    fetchStats()
  ]);
  
  return {
    users: users.status === "fulfilled" ? users.value : [],
    posts: posts.status === "fulfilled" ? posts.value : [],
    stats: stats.status === "fulfilled" ? stats.value : null
  };
}

Global Handlers

Catch unhandled errors as a safety net.

// Catch unhandled promise rejections (browser):
window.addEventListener("unhandledrejection", (event) => {
  console.error("Unhandled rejection:", event.reason);
  event.preventDefault(); // prevent default logging
  // Send to error tracking service:
  reportError(event.reason);
});

// Node.js:
process.on("unhandledRejection", (reason, promise) => {
  console.error("Unhandled:", reason);
});

// These are SAFETY NETS, not replacements for
// proper error handling in your code!

⚠️ Global handlers are for catching bugs you missed, not for normal error handling. Always use try/catch or .catch() for expected failures.

FAQ

Common questions about async error handling.