Skip to Content
Course content

63: Error Handling in Async Code

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're at a restaurant. You place an order for a burger and a milkshake, and then you go back to your table to chat with your friends. You aren't standing at the counter staring at the chef; you're doing other things while the food is being prepared. That's the essence of async code. But here's the catch: what happens if the kitchen realizes they're out of beef? If the waiter just never comes back to tell you, you'll sit there forever, wondering why your food hasn't arrived. In the coding world, that's a "hanging" promise or an unhandled rejection, and it's one of the fastest ways to crash a Node.js process or leave a user staring at a frozen loading spinner.

Mapping this to our code: placing the order is calling your async function, sitting at the table is your await keyword, and the waiter coming back to tell you the burger is unavailable is your catch block. If you don't provide a way for the "waiter" to report the error, the app just fails silently—or worse, loudly and unexpectedly.

Wrapping the Wait in a Safety Net

When you're using async/await, the most natural way to handle errors is with a try...catch block. It looks a lot like synchronous code, which is why I prefer it. You're essentially saying, "Try to do this sequence of things, and if any of them blow up, jump straight to the catch block."

async function getUserData(userId) {
  try {
    const response = await fetch(`https://api.example.com/users/${userId}`);
    
    // Pro tip: fetch doesn't actually throw an error on 404 or 500 responses.
    // It only throws if the network request itself fails. We have to check manually.
    if (!response.ok) {
      throw new Error(`User not found! Status: ${response.status}`);
    }

    const data = await response.json();
    return data;
  } catch (error) {
    console.error("Something went sideways:", error.message);
    // Depending on the app, you might return a default user or re-throw the error
    return null; 
  }
}

Notice how I manually threw an error when response.ok was false. I've seen too many junior devs assume that a 404 would automatically trigger the catch block. It doesn't. fetch only rejects the promise if there's a network failure or the server is completely unreachable.

The "All or Nothing" Trap

Now, things get trickier when you're firing off multiple requests at once. You've probably used Promise.all() to speed things up. It's great, but it has a "fail-fast" behavior. If you're fetching data for ten different users and just one of those requests fails, Promise.all() immediately rejects. It doesn't matter if the other nine were successful; the whole operation is marked as a failure.

If you need to know which specific ones failed while still keeping the successful data, Promise.allSettled() is your best friend. It doesn't throw an error; instead, it returns an array of objects describing the outcome of each promise.

async function getMultipleProfiles(ids) {
  const promises = ids.map(id => fetch(`https://api.example.com/users/${id}`));
  
  const results = await Promise.allSettled(promises);
  
  results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      console.log(`User ${ids[index]} loaded successfully`);
    } else {
      console.error(`User ${ids[index]} failed because: ${result.reason}`);
    }
  });
}

I usually lean towards allSettled when I'm building dashboards. If one widget fails to load its data, I don't want the entire page to crash—I just want that one widget to show an "Error loading data" message while the rest of the page works perfectly.

Avoiding the Global Crash

Lastly, a word of caution: never leave a promise "naked." If you're calling an async function without an await (perhaps you're firing it off in the background), you must attach a .catch() to it. If you don't, and it fails, you get an UnhandledPromiseRejectionWarning. In modern Node.js versions, this can actually terminate your entire process.

// Dangerous: if this fails, the whole app might crash
sendAnalyticsData(data); 

// Safe: we've handled the potential failure
sendAnalyticsData(data).catch(err => console.error("Analytics failed, but we'll keep going."));



📋 Practical Task

Build a Resilient Multi-City Weather Fetcher

Your task is to create a function called fetchWeatherReports that takes an array of city names. The function should attempt to fetch weather data for each city from a mock API.

Requirements:

  • Use Promise.allSettled() to ensure that one failing city request doesn't stop the others from loading.
  • Create a helper function mockFetchWeather(city) that randomly returns a successful response (a JSON object with temperature) or throws an error (e.g., "City not found") to simulate API instability.
  • The main function should return an object containing two arrays: successful (containing the data for cities that worked) and failed (containing the city names that caused errors).
  • Log the final object to the console to verify that your error handling correctly sorted the successes from the failures.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.