Happy path programming is for tutorials. Real-world applications live in a chaotic network environment. You must engineer your code to fail gracefully.
1The Inevitability of Failure
When building web applications, you must assume the network will fail. The user might drive through a tunnel. The server might run out of memory. The DNS might go down. If your fetch request is not wrapped in a try/catch block, an unhandled Promise rejection will bubble up to the top of the runtime environment, often causing a fatal crash or leaving the UI permanently stuck in a 'Loading...' state.
try {
const response = await fetch("https://api.com/data");
// Success logic here...
} catch (error) {
// Triggers ONLY on complete network failure
showErrorToUser("You are offline.");
}
2The Quirks of Fetch
One of the most confusing aspects of the Fetch API is its definition of 'success'. To fetch, a successful request simply means that a response was received from a server. It doesn't care if that response is a 200 OK (Here is your data) or a 500 Internal Server Error (The database exploded). Because a response was received, the Promise resolves. This is why you must ALWAYS manually check response.ok before attempting to parse the JSON.
try {
const res = await fetch("/users");
if (!res.ok) {
throw new Error(`HTTP Error: ${res.status}`);
}
const data = await res.json();
} catch (error) {
console.error(error.message);
}
3Decoding Status Codes
As a developer, you must memorize the three primary HTTP status code ranges. 200-level codes represent Success (e.g., 200 OK, 201 Created). 400-level codes represent Client Errors (e.g., 400 Bad Request, 401 Unauthorized, 404 Not Found). This means the client messed up the request. 500-level codes represent Server Errors (e.g., 500 Internal Error, 503 Service Unavailable). This means the client's request was fine, but the server crashed.
200 - 299 -> Success
400 - 499 -> Client Error (Your fault)
500 - 599 -> Server Error (Their fault)
