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.
tryblock: Contains the code that you expect to run successfully (the "happy path"). This is where you'll place yourawaitcalls.catchblock: If any code within thetryblock throws an error, the execution of thetryblock is immediately stopped, and control is passed to thecatchblock. 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.

For a more detailed explanation with code examples, please read the following section from The Modern JavaScript Tutorial.
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.

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.
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 thetryblock. - If
loadJsonsucceeds, thebreakstatement exits thewhileloop. - If
loadJsonthrows an error, thecatchblock 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, wethrowit 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...catchblock is the idiomatic way to handle errors inasyncfunctions. - A single
try...catchblock can gracefully handle failures from multipleawaitcalls in a sequence. - You can inspect the error object in the
catchblock 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
asyncfunction, 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.