Lesson 29 of 80
try/catch, promise rejection, and async/await error handling are three faces of the same mechanism — and custom Error subclasses are how you make failures carry useful, specific information.
12 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
Question 1 of 2
Inside an async function, can a regular `try/catch` catch a rejected promise from `await fetch(...)`?
Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
class ValidationError extends Error {
constructor(message, field) {
super(message); // sets this.message, wires up the stack trace
this.name = "ValidationError";
this.field = field; // custom data specific to this error type
}
}
function validate(input) {
if (!input.email) throw new ValidationError("email is required", "email");
}
try {
validate({});
} catch (err) {
if (err instanceof ValidationError) {
console.log(`${err.field}: ${err.message}`);
} else {
throw err; // don't swallow errors you don't know how to handle
}
}
extends Error gets you .message, .stack, and correct instanceof behavior — you're using the prototype-chain-based inheritance from the prototypes lesson, applied to error types.
async function loadUser(id) {
try {
const res = await fetch(`/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
console.error("Failed to load user:", err.message);
throw err;
}
}
An awaited promise's rejection becomes a regular throw inside the async function — the same try/catch you'd use for synchronous code catches it. This only works with await; a plain (non-awaited) .then() chain needs its own .catch().
await fetch(...) rejects. Inside a try block, that rejection becomes a normal thrown exception.
throw the rest.finally block always runs, whether the try succeeded, threw, or even returned early — it's the right place for cleanup (closing a connection, hiding a loading spinner) that must happen either way.Promise.reject(new Error(...)) and throw inside an async function are equivalent — both produce a rejected promise, which is why try/catch around await correctly catches both."Design an error-handling strategy for an API layer" tests whether you distinguish expected failures (validation, 404) that deserve custom error types from truly unexpected ones that should propagate loudly rather than being swallowed.