Error Handling in JavaScript: Try, Catch, Finally

Open the browser console on almost any website for long enough, and you’ll eventually see a red error message. Something failed — a value was undefined when the code expected an object, a network request timed out, a function got called with the wrong type of argument. The difference between an application that crashes and one that fails gracefully almost always comes down to one thing: whether the developer anticipated that failure and handled it deliberately. That's exactly what try, catch, and finally are built for.
What Errors Are in JavaScript
An error in JavaScript is what happens when something goes wrong while code is running — an operation that simply can’t complete as written. Common, everyday examples:
null.toUpperCase();
// TypeError: Cannot read properties of null
JSON.parse("{ invalid json");
// SyntaxError: Unexpected token i in JSON
undefinedFunction();
// ReferenceError: undefinedFunction is not definedThe practical examples make try, catch, and finally easy to understand, especially the loading-spinner example. One small addition that could make this even more useful would be mentioning the difference between synchronous errors and errors in asynchronous code, particularly how try/catch works with async/await.
This is exactly the gap try/catch exists to close — turning "the program stops" into "the program responds sensibly and keeps going."
Using Try and Catch Blocks
A try block wraps code that might throw an error. If it does, execution immediately jumps to the matching catch block instead of crashing the program.
try {
const data = JSON.parse(userInput);
console.log(data);
} catch (error) {
console.log("Something went wrong parsing that input:", error.message);
}If userInput isn't valid JSON, JSON.parse throws — but instead of stopping the whole program, execution jumps straight into the catch block, where error holds information about exactly what went wrong.
What happens step by step
A practical example: fetching data safely
try {
const response = await fetch("/api/user");
const user = await response.json();
console.log(user);
} catch (error) {
console.log("Couldn't load the user right now. Please try again.");
}Without the try/catch here, a failed network request would throw an unhandled error — with it, the application can show a sensible message instead of breaking entirely.
The Finally Block
A finally block runs after the try/catch, no matter what happened — whether the try block succeeded, or an error was thrown and caught.
function loadData() {
console.log("Starting...");
try {
riskyOperation();
console.log("Success!");
} catch (error) {
console.log("Failed:", error.message);
} finally {
console.log("Cleanup: this always runs.");
}
}Whether riskyOperation() succeeds or throws, the message inside finally always prints — making it the natural place for cleanup work that needs to happen regardless of the outcome: closing a connection, hiding a loading spinner, releasing a resource.
A real use case: a loading indicator
async function loadUserProfile() {
showSpinner();
try {
const user = await fetchUser();
renderProfile(user);
} catch (error) {
showErrorMessage("Couldn't load your profile.");
} finally {
hideSpinner(); // runs whether the fetch succeeded or failed
}
}Without finally, you'd need to remember to call hideSpinner() in both the success path and the error path separately — finally guarantees it happens exactly once, either way.
Throwing Custom Errors
Beyond catching errors JavaScript throws automatically, you can deliberately throw your own — useful for signaling that your own code has hit a condition it considers invalid, even if nothing technically crashed yet.
function withdraw(balance, amount) {
if (amount > balance) {
throw new Error("Insufficient funds");
}
return balance - amount;
}
try {
withdraw(100, 500);
} catch (error) {
console.log(error.message); // "Insufficient funds"
}Building more specific custom error types
For larger applications, extending JavaScript’s built-in Error class lets you create your own, more specific error types:
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
function validateAge(age) {
if (age < 0) {
throw new ValidationError("Age cannot be negative");
}
}
try {
validateAge(-5);
} catch (error) {
if (error instanceof ValidationError) {
console.log("Validation issue:", error.message);
} else {
console.log("Unexpected error:", error.message);
}
}This pattern lets a catch block distinguish what kind of problem occurred, and respond differently depending on the specific error type — rather than treating every failure identically.
Why Error Handling Matters
Graceful failure over hard crashes
An application that anticipates likely failure points — a network request timing out, invalid user input, a missing file — can respond with a clear message and keep functioning, instead of breaking entirely at the first unexpected condition.
Better debugging
A well-placed catch block that logs meaningful context (not just "something broke," but what broke and why) turns a mysterious bug report into something a developer can actually trace and fix quickly.
Protecting the user experience
Users rarely need — or want — to see a raw stack trace. Thoughtful error handling lets an application show a clear, friendly message (“couldn’t save your changes, please try again”) instead of a broken page or silent failure.
Preventing small failures from becoming big ones
Without error handling, one unexpected failure (a single malformed API response, for instance) can crash an entire process. Catching it at the right level contains the damage to just the part of the application that actually failed.
Final Takeaway
try, catch, and finally aren't just syntax to memorize — they're a direct answer to a simple reality: things will go wrong, and the only real choice is whether your code has already decided how to respond. try marks the code that might fail. catch gives you a deliberate place to respond instead of crashing. finally guarantees cleanup happens no matter what. And custom errors let your own code speak clearly about what specifically went wrong, instead of leaving that ambiguous. Put together, that's the difference between an application that breaks and one that simply handles it.
Frequently Asked Questions
Does a catch block stop an error from happening at all?
> No — the error still occurs; catch simply intercepts it before it crashes the program, giving your code a chance to respond deliberately instead of letting it propagate unhandled.
Can I have a try block without a catch block? Yes —
> try/finally (without catch) is valid, and useful when you want guaranteed cleanup but intend to let the error propagate further up, rather than handling it at this specific point.
Should I wrap every single line of code in try/catch?
> No — that adds unnecessary noise and can make code harder to read. Reserve try/catch for operations that can genuinely fail in ways worth handling deliberately, like network requests, parsing external data, or risky calculations.
What’s the difference between a regular Error and a custom error class?
> A regular Error is generic — just a message. A custom error class (extending Error) lets you attach a specific type/name, letting catch blocks distinguish between different kinds of failures and respond to each appropriately, rather than treating every error identically.
Originally published by Mr Madhukar
Read the complete article on Medium with full formatting & reader responses.