Hello! Let's dive back into asynchronous JavaScript.
In our last lesson, you learned how to send data to a server using fetch with POST requests. We also established a foundational error-handling pattern using try...catch and checking the response.ok property.
Today, we're going to build on that foundation to handle errors more professionally. A robust web application doesn't just crash or log an error to the console; it understands why a request failed and provides helpful feedback to the user. This lesson will teach you how to distinguish between different types of network and HTTP errors and how to handle them gracefully, a critical skill for any web developer.
By the end of this lesson, you will be able to handle various HTTP response status codes and network errors gracefully in your applications.
The Most Common fetch Misconception
In the previous lesson, we put our fetch call inside a try...catch block. A common assumption is that if an API returns an error, like a 404 Not Found, the catch block will automatically execute. This is incorrect.
The catch block is only triggered by network errors—for example, if the user loses their internet connection, the DNS can't find the server, or a CORS policy blocks the request.
If the server successfully sends a response—even a response that says "error!" (like a 404 or 500)—the fetch promise is considered fulfilled. Our try block will continue to execute, which can lead to unexpected crashes when we try to parse a response that isn't what we expected.
Let's watch a short video that explains and demonstrates this "gotcha" perfectly.
JavaScript Fetch API - One Mistake I ALWAYS MAKE!
In this video, 'JavaScript Fetch API - One Mistake I ALWAYS MAKE!', James Q Quick clearly demonstrates why a non-200 status code doesn't trigger the catch block and how to handle it correctly.
Please watch the full video (00:00 - 03:47). Pay close attention to the distinction he makes between a network error that triggers the catch block and an HTTP status error that does not.
As the video showed, the key is to inspect the Response object. Even for a 404 Not Found error, fetch gives you a Response object. This is why our check from the last lesson, if (!response.ok), is so important.
The image below shows what this looks like in a browser's developer tools. The request results in a 404 Not Found, but the JavaScript code still receives a Response object where response.ok is false and response.status is 404.

Decoding HTTP Status Codes
To handle errors gracefully, we need to know what they mean. An error saying "You sent the wrong data" (400 Bad Request) should be handled differently from an error saying "The server is down" (503 Service Unavailable).
HTTP status codes are grouped into categories:
- 2xx (Success): The request was successful (e.g.,
200 OK,201 Created). - 3xx (Redirection): You're being redirected elsewhere.
fetchhandles these automatically. - 4xx (Client Error): You, the client, did something wrong (e.g.,
404 Not Found,401 Unauthorized). - 5xx (Server Error): The server failed to fulfill a valid request (e.g.,
500 Internal Server Error).
This next video provides a quick and clear explanation of the most common status codes you'll encounter.
HTTP Status Codes Explained In 5 Minutes
The video 'HTTP Status Codes Explained In 5 Minutes' from ByteByteGo gives a great overview of what different status code families mean and covers the essential codes every developer should know.
Watch from 00:20 to 03:30. This will cover the 2xx, 4xx, and 5xx status codes, which are the most relevant for API error handling.
The Centralized Error Handling Pattern
Now let's combine these concepts into a robust, reusable pattern. The goal is to handle all errors—both network and HTTP—in one place: the catch block.
Here's how it works:
- Wrap your
asynclogic in atry...catchblock. This will handle any true network failures. - Inside the
tryblock,awaitthefetchcall. - Check if
response.okisfalse. - If it's false,
throwa newError. This is the key step. Throwing an error inside atryblock immediately stops execution and passes control to thecatchblock. We can include thestatusin the error message for more context.
This pattern ensures that any failure, network or HTTP, ends up in the catch block, where you can decide how to update the UI and inform the user.
The following article explains this pattern clearly.
Fetch API, do you really know how to handle errors?
This article, 'Fetch API, do you really know how to handle errors?', reinforces the concepts we've discussed and provides a clear example of the centralized error handling pattern.
Please read the section titled 'Best way to handle errors using Fetch API', focusing on 'Example 02'. This shows exactly how to throw an error based on the response status to handle it in the catch block.
Here is a complete code example demonstrating this pattern. We'll use a switch statement to provide more specific error messages, making our error handling even more "graceful."
async function getUser(userId) {
try {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
// Centralize error handling by throwing an error on bad responses
if (!response.ok) {
// Create an error object that includes the status
const error = new Error(`HTTP Error: ${response.status}`);
error.status = response.status;
throw error;
}
const data = await response.json();
console.log("User data:", data);
// In a real app, you would update the UI here:
// displayUserData(data);
} catch (error) {
console.error("Failed to fetch user:", error.message);
// Update the UI with a user-friendly message based on the error
// We check `error.status` for HTTP errors, and handle network errors otherwise
let userMessage;
switch (error.status) {
case 404:
userMessage = "User not found. Please check the ID and try again.";
break;
case 500:
userMessage = "The server is having problems. Please try again later.";
break;
default:
// This will catch network errors and other unexpected HTTP errors
userMessage = "An unexpected error occurred. Please check your network connection.";
}
// In a real app, you would display this message on the page:
// displayErrorMessage(userMessage);
console.log("User-facing message:", userMessage);
}
}
// Let's test it
getUser(1); // Should succeed
getUser(999); // Should result in a 404 Not Found
Test your understanding!
You are building a function getPost(postId) to fetch a blog post. Modify the function below to implement the centralized error handling pattern.
Your function should:
- Throw an error if
response.okisfalse. - In the
catchblock, log a specific message to the console for the user:- If the status is 404:
"Post not found." - If the status is 500:
"Server error. Please try again later." - For any other error (including network errors):
"Could not fetch post."
- If the status is 404:
async function getPost(postId) {
const url = `https://jsonplaceholder.typicode.com/posts/${postId}`;
try {
const response = await fetch(url);
// Your logic to check the response and throw an error goes here
const post = await response.json();
console.log("Success:", post.title);
} catch (error) {
// Your logic to log user-friendly messages goes here
}
}
// Test cases
getPost(1); // Should succeed
getPost(-1); // Should give a 404
// To simulate a network error, you could try an invalid URL like:
// const url = `https://thisserverdoesnotexist.dev/posts/${postId}`;
Show answer
async function getPost(postId) {
const url = `https://jsonplaceholder.typicode.com/posts/${postId}`;
try {
const response = await fetch(url);
if (!response.ok) {
const error = new Error(`HTTP Status: ${response.status}`);
error.status = response.status;
throw error;
}
const post = await response.json();
console.log("Success:", post.title);
} catch (error) {
switch (error.status) {
case 404:
console.log("Post not found.");
break;
case 500:
console.log("Server error. Please try again later.");
break;
default:
console.log("Could not fetch post.");
break;
}
}
}
Advanced Graceful Handling: Timeouts and Retries
To make your application truly professional, consider cases where the network is just slow, or the server fails temporarily. The following article introduces two powerful techniques.
Error Handling with Async Await in JavaScript
The article 'Error Handling with Async Await in JavaScript' provides excellent, practical code examples for implementing timeouts and retries, which are hallmarks of a resilient application.
Please read the sections 'Retrying Failed Requests' and 'Setting Up Timeouts for Long Requests'. You don't need to memorize this code now, but focus on understanding the concept behind each technique. These are powerful patterns you will use in future projects.
- Timeouts (
AbortController): This prevents your app from hanging indefinitely if a server is unresponsive. You start a timer and afetchrequest at the same time. If the timer finishes first, you "abort" the request. - Retries: If you get a temporary server error (like
503 Service Unavailable), you can automatically wait a moment and try the request again. This can make your application feel much more reliable to the user.
Conclusion
You've now moved from basic error detection to sophisticated, graceful error handling. This skill is non-negotiable for building production-quality web applications.
Key Takeaways:
- A
fetch()promise only rejects on a network error, not on HTTP error codes like 404 or 500. - Always check the
response.okproperty to see if a request was successful (status 200-299). - The best practice is to
throwa newErrorif!response.ok, allowing you to centralize all error logic in a singlecatchblock. - Use
response.statuswithin your error handling logic to provide specific, user-friendly feedback. - Techniques like timeouts and retries can further improve your application's resilience.
In our next lesson, we will put all of this together. You will build a weather dashboard that fetches data from a public API based on user input, applying everything you've learned about async/await, fetch, and robust error handling to create your first practical, API-driven project.