JS Try Catch Tutorial Advanced Error Handling
try...catch catches runtime errors so your code can recover instead of crashing. Learn the full syntax, finally cleanup, nested handling, conditional catch, and rethrowing patterns.
A try...catch statement lets you run code that might fail and recover from the failure instead of letting it crash your script. When an error is thrown inside the try block, execution jumps straight to the catch block, where you can log it, show a message, or attempt a fix.
Without this statement, any unhandled runtime error stops the rest of your code from running. With it, you decide what happens next.
try {
const data = JSON.parse("not valid json");
console.log("This never runs");
} catch (error) {
console.log("Parsing failed:", error.message);
// Parsing failed: Unexpected token 'o', "not valid json" is not valid JSON
}
console.log("This still runs after the catch block");JSON.parse throws a syntax error when given invalid input. The catch block catches that error, logs a message, and the code after the statement continues normally as shown by the last line and its comment above. Without the wrapper, that error would have terminated the script before that last line ever ran.
Try, catch, and finally syntax
A try block can pair with catch, finally, or both. Pick the shape based on whether you need to react to the error, clean up afterward, or both.
try {
riskyStep();
} catch (error) {
console.log("Handled:", error.message);
} finally {
console.log("Always runs, error or not");
}Add finally when you always need cleanup to happen, such as closing a file or clearing a loading spinner. Drop catch entirely and use try with only finally when you want cleanup to run but you want the error to keep propagating up to the caller.
If you do not need the error object itself, the catch binding is optional in modern JavaScript:
function isValidJSON(text) {
try {
JSON.parse(text);
return true;
} catch {
return false;
}
}This shorter form is common for boolean checks like isValidJSON, where the only thing that matters is whether an error happened, not what it says.
The error object
Inside catch, the caught value is usually an Error instance with three useful properties: name, message, and stack. The stack property is the most useful for debugging, since it shows the exact line and call chain where the error originated.
| Property | Description |
|---|---|
| name | The error type, such as TypeError, ReferenceError, SyntaxError, or Error |
| message | A human-readable description of what went wrong |
| stack | A string trace of function calls leading to the error (non-standard but available everywhere) |
try {
undefinedVariable.property;
} catch (error) {
console.log(error.name); // "ReferenceError"
console.log(error.message); // "undefinedVariable is not defined"
}Reading undefinedVariable before it exists anywhere in scope throws a reference error. Checking error.name first is how the conditional catch pattern later in this article decides which branch to run.
The finally block
finally runs no matter what happens in try or catch. Even if the try block throws, returns, or breaks, finally executes before control leaves the construct.
function readConfig(path) {
const file = openFile(path);
try {
return parseConfig(file);
} catch (error) {
return { defaults: true };
} finally {
closeFile(file);
}
}This is the correct pattern for resource cleanup: open the resource, use it inside try, close it inside finally. The file closes whether parseConfig succeeds, fails, or the function returns early.
One important caveat: a return or throw inside finally overrides any return or throw from try or catch.
function demo() {
try {
return "from try";
} finally {
return "from finally";
}
}
console.log(demo());This logs "from finally", not "from try", because the return inside finally wins. Avoid control flow statements in finally. Keep it for cleanup only: closing files, removing event listeners, clearing timers, or resetting state.
Conditional catch with instanceof
A single catch block handles every type of error by default. Use instanceof to respond differently depending on which error type was thrown:
try {
processUserInput(input);
} catch (error) {
if (error instanceof TypeError) {
showMessage("Please check your input format.");
} else if (error instanceof RangeError) {
showMessage("The value is outside the allowed range.");
} else {
console.error(error);
showMessage("Something unexpected happened.");
}
}This pattern is called conditional catch. You handle specific errors you expect and let unexpected ones surface to a higher-level handler or a crash reporter, instead of treating every failure the same way.
Rethrowing errors
Sometimes you want to catch an error, do something with it, and then let it continue up the call stack as if it was never caught. Log the details, then throw the same error object again:
try {
saveToAPI(data);
} catch (error) {
console.error("API save failed, details:", error);
throw error;
}Rethrowing is also how you filter errors inside a conditional catch. If an error is not one you know how to handle, send it back up instead of swallowing it:
try {
performOperation();
} catch (error) {
if (error instanceof ValidationError) {
handleValidation(error);
} else {
throw error;
}
}This prevents silently hiding errors that should be handled by a caller further up the chain, which is one of the most common sources of hard-to-debug production bugs.
Nested try...catch
You can nest one try...catch inside another. An error thrown in the inner block is caught by the nearest enclosing catch. If the inner catch rethrows, the outer catch receives it next:
try {
try {
throw new Error("inner problem");
} catch (error) {
console.log("Inner catch:", error.message); // "Inner catch: inner problem"
throw error;
} finally {
console.log("Inner finally"); // runs before the outer catch sees the error
}
} catch (error) {
console.log("Outer catch:", error.message); // "Outer catch: inner problem"
}The three log lines fire in order: the inner catch first, then the inner finally, then the outer catch, since the rethrow only reaches the outer block after the inner finally has finished running.
The outer catch only fires if the error escapes the inner catch, either because the inner block has no catch or because the inner catch rethrows, as shown by the two paths in the diagram above.
try...catch with async code
try...catch only catches synchronous errors. Errors thrown inside a setTimeout or setInterval callback are not caught unless the try...catch wraps the callback itself, not the call that schedules it:
setTimeout(() => {
try {
throw new Error("Timer error");
} catch (error) {
console.log("Caught:", error.message);
}
}, 1000);Wrapping the outer setTimeout call in try...catch would not work, because the callback runs later on its own turn of the event loop, after the surrounding try block has already finished. For promises and async/await, try...catch works directly with await:
async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Failed to fetch user:", error.message);
return null;
}
}When you await a promise, a rejection becomes a synchronous throw that try...catch can handle. Without await, you need .catch() on the promise chain instead. For more on async patterns, see the JavaScript Promises guide and async/await tutorial.
Common mistakes
Catching too broadly is the most common mistake. A catch block that silently swallows every error hides real bugs instead of surfacing them:
try {
doWork();
} catch (e) {
if (e instanceof ExpectedError) {
handleExpected(e);
} else {
console.error(e);
}
}An empty catch block with no branching, like catch (e) {}, throws away information you will need later when something breaks in production. Always log or rethrow errors you did not expect.
Putting too much code inside the try block is the second common mistake. The try block should contain only the code that might throw. Non-throwing setup code, like sending a notification after everything else succeeded, belongs outside it:
function process() {
let config, result;
try {
config = loadConfig();
result = compute(config);
save(result);
} catch (e) {
console.error(e);
return;
}
sendNotification();
}Keeping sendNotification outside the try block makes it clear that notification failures are a separate concern from the operations that can actually fail during processing.
Forgetting that event handlers need their own try...catch is the third mistake. Each event listener or timer callback runs in its own execution context, so the wrapper has to go inside the handler body, not around the call that registers it:
button.addEventListener("click", () => {
try {
riskyOperation();
} catch (e) {
console.error("Click handler failed:", e);
}
});Wrapping addEventListener itself in try...catch would never catch anything, because addEventListener only registers the handler. It does not run riskyOperation until the user actually clicks the button, well after the surrounding try block has exited.
Rune AI
Key Insights
- try...catch catches runtime errors and lets your code recover instead of crashing.
- The finally block always runs, making it the right place for resource cleanup.
- Use instanceof to conditionally handle specific error types and rethrow unexpected ones.
- try...catch only catches synchronous errors inside the try block, not errors in timers or promises unless you wrap the callback.
- Avoid return, throw, break, or continue inside finally, they override the original control flow.
Frequently Asked Questions
Does try...catch catch all JavaScript errors?
Can I use try...catch without a catch block?
What happens if I return inside a finally block?
Conclusion
try...catch is the backbone of resilient JavaScript. Use it to handle expected failures gracefully, clean up resources with finally, and decide which errors to handle versus rethrow. Combined with the Error constructor and custom error types, it gives you full control over how your program responds when things go wrong.
More in this topic
Using Decorators for Logging in JS Architecture
Learn how JavaScript decorators wrap class methods to add logging without touching the original code, including setup with Babel and how execution order works.
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.