Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 21

Error Handling with Async/Await

Recommended resources

Hello! Welcome to the next lesson on your journey to mastering modern JavaScript.

Introduction

In our last session, you learned how to use async/await to write asynchronous code that reads like a clean, synchronous sequence of operations. We briefly touched on the idea that errors are handled using a try...catch block.

Today, we will dive deep into that concept. Your learning outcome is to handle errors in async/await functions using try/catch blocks. Since you come from a Laravel background, the try...catch structure should be a familiar pattern for error handling. Our goal today is to solidify how to apply this pattern effectively in the context of asynchronous JavaScript.

By the end of this lesson, you'll be able to write robust async functions that can gracefully handle network failures, API errors, and other unexpected issues.


Why try...catch? The Core Mechanism

The key to understanding error handling with async/await is knowing what happens when you await a Promise that gets rejected.

  • In a .then() chain, a rejection passes control to the next .catch() handler.
  • With async/await, when a rejected Promise is awaited, it throws an error at that exact line.

Because an error is thrown, we can use JavaScript's standard, built-in mechanism for handling thrown errors: the try...catch block.

Let's start with a quick, clear video demonstration.

JavaScript ASYNC/AWAIT is easy! ⏳

This short clip from Bro Code demonstrates exactly how to wrap async/await code within a try...catch block. Notice how the code that might fail is placed in the try block.

Watch the segment from 03:32 to 04:11. It shows what happens when a promise rejects and how the catch block intercepts the error.


The Control Flow of try...catch

The try...catch statement provides a structured way to handle potential errors. It consists of a try block and a catch block. The finally block is optional and will not be our focus today.

  1. try block: Contains the code that you expect to run successfully (the "happy path"). This is where you'll place your await calls.
  2. catch block: If any code within the try block throws an error, the execution of the try block is immediately stopped, and control is passed to the catch block. The error object is passed as an argument to this block, so you can inspect it and decide how to respond.

This flowchart provides a great visual of the execution path.

Try-Catch-Finally Flowchart for Error Handling
A flowchart showing the execution path of a `try...catch` block. Code in the 'Try Block' is executed. If an error occurs, control moves to the 'Catch Block'. If not, the 'Catch Block' is skipped. The optional 'Finally Block' executes regardless.

For a more detailed explanation with code examples, please read the following section from The Modern JavaScript Tutorial.

Async/await - Error handling

This article explains the concept clearly and provides excellent code snippets.

Please read the section titled Error handling. Focus on how a rejected promise in an await expression is equivalent to a throw statement, and see how try...catch is used to handle it.


Handling Multiple Asynchronous Steps

One of the great advantages of using try...catch is its ability to handle errors from multiple asynchronous operations within a single block. If you have several await calls in a sequence, you don't need a separate error handler for each one. A single catch block will handle a failure from any of them.

Consider this example where we fetch user data and then, based on that, fetch their posts.

async function getUserAndPosts(userId) {
  try {
    console.log('Fetching user...');
    const userResponse = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
    if (!userResponse.ok) {
      // If the user doesn't exist, this will throw an error
      throw new Error(`User not found. Status: ${userResponse.status}`);
    }
    const user = await userResponse.json();
    console.log(`User found: ${user.name}`);

    console.log('Fetching posts for user...');
    // Let's introduce an error here by using a bad URL
    const postsResponse = await fetch(`https://jsonplaceholder.typicode.com/posts_bad_url?userId=${userId}`);
    if (!postsResponse.ok) {
      throw new Error(`Failed to fetch posts. Status: ${postsResponse.status}`);
    }
    const posts = await postsResponse.json();
    
    console.log(`Found ${posts.length} posts.`);
    return { user, posts };

  } catch (error) {
    // This single catch block will handle errors from EITHER fetch call
    console.error('An error occurred:', error.message);
    // You could show an error message to the user here
  }
}

// Run the function
getUserAndPosts(1);

If you run this code, the first fetch for the user succeeds, but the second one for posts fails. The execution immediately jumps to the catch block, and you'll see the "An error occurred" message.

This image shows a similar, complete example.

Error Handling with Async/Await and Try/Catch
A JavaScript code snippet showing an `async` function `getWeatherForUser` that fetches data. The `await` calls are wrapped in a `try` block, and any potential errors are caught and logged in the `catch` block.

Practical Application: Refactoring to try...catch

Let's apply what you've learned by refactoring a piece of code that uses .then().catch() into the async/await style.

The task from the javascript.info article provides a perfect real-world scenario: fetching data from the GitHub API and re-prompting the user if the user doesn't exist.

Here is the original code using Promises:

// ... (HttpError class definition) ...

function demoGithubUser() {
  let name = prompt("Enter a name?", "iliakan");

  return loadJson(`https://api.github.com/users/${name}`)
    .then(user => {
      alert(`Full name: ${user.name}.`);
      return user;
    })
    .catch(err => {
      if (err instanceof HttpError && err.response.status == 404) {
        alert("No such user, please reenter.");
        return demoGithubUser(); // Recursive call to retry
      } else {
        throw err; // Rethrow other errors
      }
    });
}

Now, let's examine the refactored solution using async/await and try...catch. Pay close attention to how the recursive retry logic is transformed into a while loop, which is often cleaner.

Rewrite with async/await

Now, let's study the refactored version of the GitHub user-fetching function.

Find the task titled Rewrite "rethrow" with async/await. Read the problem description and then study the provided solution. Notice how the while(true) loop, try...catch, and break statement work together to create the retry logic.

The key takeaways from this refactoring are:

  • The core logic (await loadJson(...)) is placed inside the try block.
  • If loadJson succeeds, the break statement exits the while loop.
  • If loadJson throws an error, the catch block is executed.
  • Inside catch, we can check the type of error. If it's a 404, we alert the user and the loop continues, prompting again. For any other error, we throw it to let it be handled by an outer error handler (or stop the script).

Conclusion

You have now mastered the primary method for error handling with async/await. By using try...catch, you can write asynchronous code that is not only clean and readable but also resilient to failure.

Key Takeaways:

  • Awaiting a rejected Promise throws an error.
  • The standard try...catch block is the idiomatic way to handle errors in async functions.
  • A single try...catch block can gracefully handle failures from multiple await calls in a sequence.
  • You can inspect the error object in the catch block to implement conditional logic, such as retrying an operation or showing a specific message to the user.
  • If you don't catch an error inside an async function, the Promise that the function returns will be rejected, leading to an "unhandled promise rejection" error in the console.

Preview of Next Lesson:
You are now fully equipped to work with APIs. In the next lesson, we will focus on using the Fetch API with async/await to make GET and POST requests to a server. We'll combine everything you've learned about async/await and error handling to build functions that reliably communicate with a backend service.