Error Handling in Async Code
Objectives
By the end of this chapter, you should be able to:
- Catch a rejected promise with
try/catcharoundawait - Turn a bad HTTP status into a real error you can catch
- Explain what happens when one promise in a
Promise.all()rejects
💡 Why this matters: Every network request can fail, a bad connection, a server error, a typo in a URL. Code that only handles the success case breaks the moment something goes wrong. This lesson makes failure a normal, handled case instead of a crash.
Catching a Rejected await
await on a rejected promise throws, so ordinary try/catch works directly.
async function getUser() {
const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
return response.json();
}
async function main() {
try {
const user = await getUser();
console.log("Got user:", user.name);
} catch (error) {
console.log("Something went wrong:", error.message);
}
}
main();
If the network request genuinely fails (no connection, DNS failure, and similar), fetch()’s promise rejects, await throws, and catch handles it. This is the same try/catch from Course 1, applied to an awaited line instead of a synchronous one.
Turning a Bad Status Into a Real Error
From last lesson: fetch() does not reject on a 404 or 500, those are still “successful” requests as far as fetch() is concerned. To catch them the same way, check response.ok and throw yourself.
async function getUser(id) {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
return response.json();
}
async function main() {
try {
const user = await getUser(1);
console.log("Got user:", user.name);
} catch (error) {
console.log("Caught:", error.message);
}
}
main();
Now both failure kinds, a network failure that makes fetch() itself reject, and a bad status that fetch() resolves normally, end up in the same catch block. The calling code doesn’t need to know or care which one happened, it just handles “the request didn’t work.”
Error Handling with .then()/.catch()
If you’re using a promise chain instead of async/await, .catch() catches a rejection from anywhere earlier in the chain, including a throw inside a .then().
function getUser(id) {
return fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
.then((response) => {
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
return response.json();
})
.catch((error) => {
console.log("Caught:", error.message);
});
}
getUser(1);
A throw inside any .then() in the chain skips every remaining .then() and jumps straight to the next .catch(), the same way a rejected promise does.
Promise.all() and Rejection
If any promise passed to Promise.all() rejects, the whole thing rejects immediately, with that error, even if the other promises haven’t finished yet.
async function loadEverything() {
try {
const [users, posts, todos] = await Promise.all([
fetch("https://jsonplaceholder.typicode.com/users").then((r) => r.json()),
fetch("https://jsonplaceholder.typicode.com/posts").then((r) => r.json()),
fetch("https://jsonplaceholder.typicode.com/todos").then((r) => r.json()),
]);
console.log(`Loaded ${users.length} users, ${posts.length} posts, ${todos.length} todos`);
} catch (error) {
console.log("At least one request failed:", error.message);
}
}
loadEverything();
If the posts request fails, loadEverything() doesn’t wait around for todos to finish, Promise.all() rejects right away, and you land in catch without knowing whether users or todos would have succeeded. This is worth knowing going in: Promise.all() is “all or nothing,” useful when every piece of data is required, less useful when you’d rather see whichever results succeeded.
Try It
Starter code for all three exercises:
function delayedValue(value, ms) {
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}
function delayedFailure(ms) {
return new Promise((_, reject) => setTimeout(() => reject(new Error("failed")), ms));
}
- Write an
asyncfunction that awaitsdelayedFailure(500)inside atry/catch, and logs the caught error’s message. - Write an
asyncfunctiongetUser(id)that fetches`https://jsonplaceholder.typicode.com/users/${id}`, throws an error ifresponse.okisfalse, and returns the parsed JSON otherwise. Call it with1(should succeed) and then with9999(should throw, JSONPlaceholder only has 10 users), both inside atry/catch. - Use
Promise.all()withdelayedValue("A", 100),delayedFailure(50), anddelayedValue("C", 150)inside atry/catch. Confirm thecatchblock runs, and that it happens before all three promises would have finished.
Recap
try/catcharound anawaitcatches a rejected promise directly, the same as any thrown error.fetch()doesn’t reject on a bad HTTP status. Checkresponse.okandthrowyourself to route that case into the same error handling as a network failure..catch()on a promise chain catches a rejection from anywhere earlier in the chain, including athrowinside a.then().- If any promise in
Promise.all()rejects, the whole thing rejects immediately, without waiting for the others to settle.
Next lesson: this module’s exercises, putting timers, promises, async/await, and fetch together.