Error Handling: try, catch, finally
try/catch lets your code recover from a thrown error instead of crashing, and finally always cleans up.
Loading visual…
Definition
A `try` block runs code that might fail. If any statement inside it throws, execution jumps straight to the matching `catch` block, which receives the thrown value (usually an `Error` object) and can log it, recover, or decide to throw again. Code after the point of failure inside `try` never runs, but code inside `catch` runs in its place, so the program keeps going instead of crashing. You can throw your own errors with the `throw` keyword, using a plain value or, better, an `Error` (or a subclass of it) so callers get a `message` and a stack trace. A `finally` block, if present, runs after `try`/`catch` no matter what happened: whether the `try` block finished cleanly, threw and was caught, or even threw again from inside `catch`. This makes it the right place for cleanup that must always happen, like closing a connection or hiding a loading spinner. One sharp edge is that a `return` (or `throw`) inside `finally` overrides whatever the `try` or `catch` block was about to return. With `async`/`await`, a rejected promise behaves like a thrown error, so wrapping an `await` in `try`/`catch` catches it the same way a synchronous throw would.
Examples
function safeParse(json) {
try {
return JSON.parse(json);
} catch (err) {
console.error("Bad JSON:", err.message);
return null;
}
}
safeParse("{}"); // {}
safeParse("not json"); // null, error loggedJSON.parse throws on invalid input; catch stops that error from crashing the caller and returns a safe fallback instead.
function withCleanup() {
try {
throw new Error("failed");
} finally {
console.log("cleanup runs");
}
}
// logs "cleanup runs", then the error still propagatesfinally runs even though there is no catch here; it does not swallow the error, the error still propagates after finally finishes.
async function loadUser(id) {
try {
const res = await fetch(`/users/${id}`);
if (!res.ok) throw new Error("request failed");
return await res.json();
} catch (err) {
console.error(err.message);
return null;
}
}await rejects like a synchronous throw, so the same try/catch that handles thrown errors also catches a failed fetch or a manually thrown error.
Common mistakes
- Catching every error the same way and swallowing it silently, which hides real bugs instead of fixing them.
- Forgetting that a return or throw inside finally overrides any return or throw from try or catch.
- Awaiting a promise outside of a try/catch (and with no .catch), which leaves a rejection unhandled instead of caught.
Key takeaways
- try/catch stops a thrown error from crashing the program and lets you handle it in one place.
- finally always runs, whether the try block succeeded, threw, or was caught, making it the spot for required cleanup.
- A return or throw inside finally overrides an earlier return or throw from try or catch.
- Wrapping an await in try/catch catches a rejected promise the same way it catches a synchronous throw.
function run() { try { return "from try"; } finally { return "from finally"; } } run();
Where you see this
- Parsing JSON or other user-supplied data without letting a malformed payload crash the app.
- Wrapping a network request so a failed fetch shows an error message instead of an unhandled rejection.
- Validating input by throwing a descriptive error and catching it at a boundary to show one friendly message.
- Guaranteeing cleanup code (closing a file, clearing a timer, hiding a spinner) runs whether or not the operation succeeded.