CodingNic

Asynchronous JavaScript

Error Handling in Async Code

Asynchronous JavaScript 20 min read

Error Handling in Async Code

Objectives

By the end of this chapter, you should be able to:

  • Catch a rejected promise with try/catch around await
  • Turn a bad HTTP status into a real error you can catch
  • Explain what happens when one promise in a Promise.all() rejects

💡 Why this matters: Every network request can fail, a bad connection, a server error, a typo in a URL. Code that only handles the success case breaks the moment something goes wrong. This lesson makes failure a normal, handled case instead of a crash.

Catching a Rejected await

await on a rejected promise throws, so ordinary try/catch works directly.

javascript
async function getUser() {
  const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
  return response.json();
}

async function main() {
  try {
    const user = await getUser();
    console.log("Got user:", user.name);
  } catch (error) {
    console.log("Something went wrong:", error.message);
  }
}

main();

If the network request genuinely fails (no connection, DNS failure, and similar), fetch()’s promise rejects, await throws, and catch handles it. This is the same try/catch from Course 1, applied to an awaited line instead of a synchronous one.

Turning a Bad Status Into a Real Error

From last lesson: fetch() does not reject on a 404 or 500, those are still “successful” requests as far as fetch() is concerned. To catch them the same way, check response.ok and throw yourself.

javascript
async function getUser(id) {
  const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);

  if (!response.ok) {
    throw new Error(`Request failed with status ${response.status}`);
  }

  return response.json();
}

async function main() {
  try {
    const user = await getUser(1);
    console.log("Got user:", user.name);
  } catch (error) {
    console.log("Caught:", error.message);
  }
}

main();

Now both failure kinds, a network failure that makes fetch() itself reject, and a bad status that fetch() resolves normally, end up in the same catch block. The calling code doesn’t need to know or care which one happened, it just handles “the request didn’t work.”

Error Handling with .then()/.catch()

If you’re using a promise chain instead of async/await, .catch() catches a rejection from anywhere earlier in the chain, including a throw inside a .then().

javascript
function getUser(id) {
  return fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
    .then((response) => {
      if (!response.ok) {
        throw new Error(`Request failed with status ${response.status}`);
      }
      return response.json();
    })
    .catch((error) => {
      console.log("Caught:", error.message);
    });
}

getUser(1);

A throw inside any .then() in the chain skips every remaining .then() and jumps straight to the next .catch(), the same way a rejected promise does.

Promise.all() and Rejection

If any promise passed to Promise.all() rejects, the whole thing rejects immediately, with that error, even if the other promises haven’t finished yet.

javascript
async function loadEverything() {
  try {
    const [users, posts, todos] = await Promise.all([
      fetch("https://jsonplaceholder.typicode.com/users").then((r) => r.json()),
      fetch("https://jsonplaceholder.typicode.com/posts").then((r) => r.json()),
      fetch("https://jsonplaceholder.typicode.com/todos").then((r) => r.json()),
    ]);
    console.log(`Loaded ${users.length} users, ${posts.length} posts, ${todos.length} todos`);
  } catch (error) {
    console.log("At least one request failed:", error.message);
  }
}

loadEverything();

If the posts request fails, loadEverything() doesn’t wait around for todos to finish, Promise.all() rejects right away, and you land in catch without knowing whether users or todos would have succeeded. This is worth knowing going in: Promise.all() is “all or nothing,” useful when every piece of data is required, less useful when you’d rather see whichever results succeeded.

Try It

Starter code for all three exercises:

javascript
function delayedValue(value, ms) {
  return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}
function delayedFailure(ms) {
  return new Promise((_, reject) => setTimeout(() => reject(new Error("failed")), ms));
}
  1. Write an async function that awaits delayedFailure(500) inside a try/catch, and logs the caught error’s message.
  2. Write an async function getUser(id) that fetches `https://jsonplaceholder.typicode.com/users/${id}`, throws an error if response.ok is false, and returns the parsed JSON otherwise. Call it with 1 (should succeed) and then with 9999 (should throw, JSONPlaceholder only has 10 users), both inside a try/catch.
  3. Use Promise.all() with delayedValue("A", 100), delayedFailure(50), and delayedValue("C", 150) inside a try/catch. Confirm the catch block runs, and that it happens before all three promises would have finished.

Recap

  • try/catch around an await catches a rejected promise directly, the same as any thrown error.
  • fetch() doesn’t reject on a bad HTTP status. Check response.ok and throw yourself to route that case into the same error handling as a network failure.
  • .catch() on a promise chain catches a rejection from anywhere earlier in the chain, including a throw inside a .then().
  • If any promise in Promise.all() rejects, the whole thing rejects immediately, without waiting for the others to settle.

Next lesson: this module’s exercises, putting timers, promises, async/await, and fetch together.