Things go wrong. A user types text where you expected a number, a file isn’t there, a server returns nonsense, a piece of data you were sure existed turns out to be undefined. When something like that happens, JavaScript throws an error — and if nothing catches it, that error stops your script in its tracks. The good news is that errors aren’t a sign you wrote bad code. They’re a normal, expected part of every real program, and JavaScript gives you a clean way to deal with them: try...catch.
In this article you’ll learn how to catch errors before they crash your page, how to create and throw your own errors when something is wrong, what the finally block is for, and a few habits that separate fragile code from code that holds up under pressure. If you’ve worked through functions and arrays and objects already, you’re ready for this.
What an error actually is
When JavaScript hits something it can’t handle, it throws an error. An error is a real object — usually an instance of Error — that carries information about what went wrong. If your code doesn’t catch it, the error propagates up and the script halts, and you’ll see a red message in the browser console.
Here’s the simplest way to provoke one:
const user = null;
console.log(user.name);
// TypeError: Cannot read properties of null (reading 'name')
user is null, so there’s no .name to read. JavaScript throws a TypeError, and any code after that line never runs. Without error handling, one bad value can take down an entire feature.
An error object has two properties you’ll use constantly:
name— the type of error, like"TypeError"or"RangeError".message— a human-readable description of what happened.
const err = new Error("Something broke");
console.log(err.name); // "Error"
console.log(err.message); // "Something broke"
The try…catch block
The core tool for handling errors is try...catch. You put the risky code inside try. If anything in there throws, JavaScript immediately jumps to the catch block instead of crashing — and hands you the error object so you can decide what to do:
try {
const user = null;
console.log(user.name); // throws a TypeError
console.log("This line never runs");
} catch (error) {
console.log("Something went wrong:", error.message);
}
console.log("The program keeps going.");
Notice two things. First, the moment user.name throws, the rest of the try block is skipped — "This line never runs" is never printed. Second, the program survives: the final console.log runs normally. That’s the whole point. Instead of the script dying, you caught the problem and carried on.
The variable in catch (error) is the error object that was thrown. You can name it anything (err, e, error), but pick something clear. From it you can read .message, .name, and more.
Optional catch binding
Since ES2019 you can omit the error variable entirely if you don’t need it: catch { ... }. It’s handy when you only want to react to the fact that something failed, not inspect the details — for example, falling back to a default value. But most of the time you do want the error, if only to log it, so reaching for catch (error) is the safer default.
Throwing your own errors
try...catch doesn’t only catch errors JavaScript creates for you. You can throw your own with the throw keyword, whenever your code detects a situation it considers invalid. This is how you enforce your own rules.
You can technically throw any value, but you should always throw an Error object (or a subclass of it). Error objects carry a stack trace and behave consistently everywhere:
function setAge(age) {
if (typeof age !== "number" || Number.isNaN(age)) {
throw new Error("Age must be a number");
}
if (age < 0) {
throw new Error("Age cannot be negative");
}
return age;
}
try {
setAge(-5);
} catch (error) {
console.log(error.message); // "Age cannot be negative"
}
When throw runs, it behaves exactly like a built-in error: execution stops at that point and jumps to the nearest enclosing catch. This lets you reject bad input loudly and early, instead of letting a wrong value silently flow deeper into your program where it causes a confusing failure later.
Throw Error objects, not strings
JavaScript lets you write throw "Age is invalid" — throwing a plain string. Don’t. A thrown string has no .message, no .name, and no stack trace, so debugging becomes painful and any code that expects error.message breaks. Always throw new Error("...") (or a more specific error type). It costs you four extra characters and saves you real headaches.
The finally block
Sometimes there’s cleanup you need to run no matter what — whether the code succeeded or failed. Closing a connection, hiding a loading spinner, re-enabling a button. That’s what the optional finally block is for. It runs after try/catch, every single time:
function loadData() {
showSpinner();
try {
const data = fetchSomething(); // might throw
return data;
} catch (error) {
console.log("Load failed:", error.message);
return null;
} finally {
hideSpinner(); // runs whether it succeeded or failed
}
}
Whether fetchSomething() works or throws, the spinner gets hidden. The finally block runs even if the try or catch block executes a return. That guarantee is exactly why it exists: it’s the reliable place for “always do this last” logic, so you can’t forget to clean up on one of the paths.
The error types you’ll meet
JavaScript has several built-in error types, all extending the base Error. Knowing them helps you read messages faster and react more precisely. The ones you’ll run into most:
TypeError— a value isn’t the type you expected. Reading a property ofnull/undefined, or calling something that isn’t a function. By far the most common in everyday code.ReferenceError— you used a variable that doesn’t exist (often a typo in the name).SyntaxError— the code itself is malformed. This usually stops the file from running at all, so you can’tcatchyour own syntax mistakes — butJSON.parseon bad input throws one you can catch.RangeError— a value is outside an allowed range, like an invalid array length.
You can check which type you caught with instanceof, which is cleaner and safer than comparing the name string:
try {
JSON.parse("{ not valid json }");
} catch (error) {
if (error instanceof SyntaxError) {
console.log("That wasn't valid JSON.");
} else {
throw error; // re-throw anything we didn't expect
}
}
That else { throw error; } line is a small but important habit: only swallow the errors you actually know how to handle, and re-throw the rest so they aren’t hidden. A catch that quietly eats every error is one of the easiest ways to bury a real bug.
A realistic example: parsing JSON safely
JSON.parse is one of the most common places real code throws. It expects a valid JSON string, and the moment it gets anything malformed, it throws a SyntaxError. If that data comes from a user, a file, or an API, you can’t assume it’s always well-formed — so wrap it:
function parseConfig(text) {
try {
return JSON.parse(text);
} catch (error) {
console.warn("Invalid config, using defaults:", error.message);
return { theme: "light", language: "en" };
}
}
const good = parseConfig('{ "theme": "dark" }');
console.log(good.theme); // "dark"
const bad = parseConfig("oops not json");
console.log(bad.theme); // "light" (fell back to defaults)
Instead of letting one bad string crash the whole app, parseConfig catches the failure and returns sensible defaults. The user never sees a broken page — they just get the standard settings. That’s graceful handling in action: anticipate that the input might be wrong, and have a plan when it is.
A JSON formatter helps you spot the problem
When JSON.parse keeps throwing and you can’t see why, paste the string into a tool that pretty-prints and checks it — a missing comma or a stray quote jumps right out. This JSON formatter is handy for exactly that: it reformats valid JSON and points at where invalid JSON breaks, so you can fix the source instead of guessing.
When to use try…catch (and when not to)
try...catch is powerful, but it isn’t the answer to every possible problem. Use it for situations that are genuinely outside your control — things you can’t prevent by writing more careful code:
- Parsing data you didn’t create (
JSON.parse, user input). - Network requests that might fail (you’ll see this constantly once you reach
fetch). - Any operation that the language documents as something that can throw.
What try...catch is not for is replacing ordinary checks. If a value might be missing, an if is clearer and cheaper than wrapping everything in a try:
// Don't lean on try...catch for this:
function getName(user) {
if (!user) {
return "Guest";
}
return user.name;
}
A plain if check communicates your intent directly and runs faster. The rule of thumb: use if to guard against conditions you can foresee and test, and use try...catch for failures you can’t predict line-by-line — operations that throw on their own.
Never silence errors with an empty catch
The most damaging pattern in error handling is the empty catch block:
try {
doSomethingImportant();
} catch (error) {
// nothing here
}This swallows the error and pretends nothing happened. The operation failed, but your program acts as if it succeeded — so the real bug surfaces somewhere far away, much later, with no trace of where it started. At an absolute minimum, log the error. Better yet, handle it, show the user something useful, or re-throw it. An empty catch doesn’t fix problems; it hides them.
Wrapping up
You now have a solid grip on handling things that go wrong:
- An error is an object (usually an
Error) with anameand amessage. Uncaught, it stops your script. try...catchruns risky code intry; if it throws, control jumps tocatch (error)instead of crashing, and your program keeps running.throw new Error("...")lets you raise your own errors to reject invalid situations. Always throwErrorobjects, never plain strings.finallyruns aftertry/catchno matter what — the reliable spot for cleanup.- Built-in types like
TypeError,ReferenceError, andSyntaxErrortell you what broke; useinstanceofto react to specific ones and re-throw what you don’t expect. - Use
try...catchfor failures outside your control (parsing, network), and plainifchecks for conditions you can foresee. Never leave acatchempty.
Error handling is what turns a demo into something people can actually rely on. With these tools you can let parts of your code fail without taking the whole thing down. Next up is JSON — the data format you just caught errors from — and once you’re comfortable there, asynchronous code with promises and fetch, where try...catch becomes an everyday companion.