Error Handling in JavaScript: Try, Catch, Finally
Every program you write will eventually encounter something unexpected. A variable that is undefined when it should not be. A network request that does not come back.
A value that is the wrong type at the wrong moment. These are not signs of bad code. They are facts of software development, and how your code responds to them is what separates programs that fail helpfully from programs that fail silently or catastrophically.
JavaScript provides a built-in mechanism for dealing with errors gracefully: try, catch, and finally. Understanding how they work, and why error handling matters, makes your code more reliable and significantly easier to debug.
What Errors Are in JavaScript
An error in JavaScript is an object. When something goes wrong at runtime, JavaScript creates an error object and throws it. If nothing catches that thrown error, it bubbles up through the call stack and eventually crashes the program or, in a browser, appears in the console as an uncaught exception.
JavaScript has several built-in error types, each representing a different category of problem.
A ReferenceError occurs when you try to access a variable that does not exist:
console.log(username); // ReferenceError: username is not defined
A TypeError occurs when a value is not the type you expected:
null.toString(); // TypeError: Cannot read properties of null
A SyntaxError occurs when JavaScript cannot parse your code at all, though these are caught before runtime. A RangeError occurs when a value falls outside an acceptable range, such as passing an invalid length to an array.
All of these share the same structure: a name property identifying the error type and a message property describing what went wrong. Knowing this matters because when you catch an error, you catch this object and can read both properties to understand what happened.
Using Try and Catch
The try block wraps code that might fail. The catch block handles the error if it does. Between the two of them, you get a controlled environment where failures are expected, intercepted, and dealt with rather than allowed to crash everything.
try {
const data = JSON.parse(userInput);
console.log(data.name);
} catch (error) {
console.log("Invalid input:", error.message);
}
Here, JSON.parse might throw a SyntaxError if userInput is not valid JSON. Without a try-catch, that error crashes the program. With it, execution jumps to the catch block the moment the error is thrown, error is bound to the error object, and you handle it on your own terms.
The important thing to understand about this flow is precision. JavaScript does not run the catch block when the try block finishes. It only runs it when an error is actually thrown. If the try block runs without any problems, the catch block is skipped entirely.
You can also inspect the error inside the catch block to respond differently depending on what went wrong:
try {
processUserData(input);
} catch (error) {
if (error instanceof TypeError) {
console.log("Received wrong data type");
} else if (error instanceof RangeError) {
console.log("Value out of acceptable range");
} else {
console.log("Something unexpected went wrong:", error.message);
}
}
This is more than defensive coding. It is documentation for your future self and your team about what kinds of failures you anticipated and how you chose to respond to each one.
The Finally Block
The finally block runs after the try and catch blocks, no matter what happened. Whether the try block succeeded, whether an error was thrown and caught, whether nothing went wrong at all: finally always executes.
function loadFile(path) {
let file = null;
try {
file = openFile(path);
processFile(file);
} catch (error) {
console.log("Failed to process file:", error.message);
} finally {
if (file) closeFile(file);
}
}
This is the right place for cleanup logic. Resources that were opened need to be closed. Connections that were established need to be released. Loading indicators need to be hidden. These things should happen regardless of success or failure, which makes finally the natural home for them.
A subtle but important behavior: finally runs even if the try or catch block contains a return statement. The function does not return until finally has finished. This means you can rely on finally for cleanup without worrying about early returns bypassing it.
function fetchData() {
try {
return getData();
} catch (error) {
return null;
} finally {
console.log("This always runs, even after a return");
}
}
The log in finally will appear before the function actually returns its value. Finally has the last word.
Throwing Custom Errors
JavaScript lets you throw anything: a string, a number, an object. But throwing an actual Error object, or a class that extends it, gives you the full stack trace and consistent structure that makes debugging much easier.
You throw an error with the throw keyword:
function divide(a, b) {
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
}
Now whoever calls divide can catch that error and respond to it. The error message you wrote will appear in error.message. The stack trace will point back to where the throw happened.
For larger applications, creating custom error classes gives you finer control:
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
function validateAge(age) {
if (typeof age !== "number") {
throw new ValidationError("Age must be a number", "age");
}
if (age < 0 || age > 120) {
throw new ValidationError("Age must be between 0 and 120", "age");
}
}
Callers can now catch this specifically with instanceof ValidationError and access the field property to know exactly which part of their input failed. Generic errors tell you something went wrong. Custom errors tell you what, where, and why.
Throwing custom errors is also a form of communication. When you define a ValidationError or a NetworkError, you are expressing intent: this is a known failure mode, and here is how to identify it.
Why Error Handling Matters
The most obvious reason to handle errors is to prevent crashes. An unhandled error in the browser freezes a page. An unhandled error in a Node.js server can take down a running process serving many users. Catching errors keeps your application alive and your users experiencing something reasonable instead of a blank screen or a failed request with no explanation.
But the deeper reason is debugging. Errors caught and logged with useful context are problems you can diagnose and fix. Errors swallowed silently, or errors that crash a program with a generic message and no context, are problems that take hours to trace back to their source.
Good error handling is also honest with users. Telling someone "We could not load your data right now, please try again" is far better than a blank page or a broken interface. Graceful failure means the rest of the application keeps working even when one part encounters a problem, and the user understands what happened and what to do next.
There is also a subtler benefit: code with proper error handling is easier to maintain. When you write a try-catch block, you are documenting that this section of code is risky, that failure is possible, and that you have thought about what to do when it happens. Future developers reading the code understand the intent immediately. Code without error handling leaves the same future developers guessing about what was assumed and what was overlooked.
Wrapping Up
Try, catch, and finally give you a structured, predictable way to deal with the unexpected. Try creates a safe zone for risky code. Catch intercepts failures and gives you a chance to respond. Finally runs cleanup no matter what. Throw lets you communicate failure conditions explicitly and precisely.
Errors are not the enemy. Unhandled errors are. A program that anticipates failure and responds to it clearly is a program you can trust, debug, and build on. Writing code that handles errors well is not a sign that you expect your code to fail. It is a sign that you understand that software operates in the real world, where things go wrong, and you have decided to be ready for it.


