JavaScript Error Handling: try, catch, throw, and finally

Errors are part of normal code, not a sign you failed. Learn how try...catch works, how to throw your own errors, what finally is for, and how to handle problems gracefully instead of crashing.

Published August 8, 20269 min readBy ACY Partner Indonesia
JavaScript error handling — try, catch, throw, and finally
300 × 250Ad Space AvailablePlace your ad here

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 of null/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’t catch your own syntax mistakes — but JSON.parse on 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 a name and a message. Uncaught, it stops your script.
  • try...catch runs risky code in try; if it throws, control jumps to catch (error) instead of crashing, and your program keeps running.
  • throw new Error("...") lets you raise your own errors to reject invalid situations. Always throw Error objects, never plain strings.
  • finally runs after try/catch no matter what — the reliable spot for cleanup.
  • Built-in types like TypeError, ReferenceError, and SyntaxError tell you what broke; use instanceof to react to specific ones and re-throw what you don’t expect.
  • Use try...catch for failures outside your control (parsing, network), and plain if checks for conditions you can foresee. Never leave a catch empty.

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.

Tags:javascriptfrontenderror handlingtry catchintermediate
728 × 90Ad Space AvailablePlace your ad here

Related Articles

See All Articles

You Might Also Like

Frontend best practices overview cover
Frontend / Fundamentals

Frontend Best Practices: An Overview

A friendly map of what good frontend really means — semantic HTML, clean CSS, unobtrusive JavaScript, responsive layouts, performance, and progressive enhancement, with pointers to go deeper.

Sep 20, 20268 min read
A Frontend Developer Roadmap cover with the path HTML to CSS to JavaScript
Frontend / Fundamentals

A Frontend Developer Roadmap: What to Learn, and in What Order

A calm, step-by-step learning path for beginners: HTML, CSS, JavaScript, developer tools, responsive and accessible design, a framework, TypeScript, and deployment, with the reasoning behind each step.

Sep 20, 202611 min read
Title card reading Styling and Interactivity over a dark blue ACY Partner background
Frontend / Fundamentals

How Styling and Interactivity Work

A beginner-friendly look at how CSS turns plain HTML into a designed layout, and how JavaScript adds behavior — the structure, style, behavior mental model for building web pages.

Sep 20, 20269 min read
Three front-end layers combining on one web page
Frontend / Fundamentals

How HTML, CSS, and JavaScript Work Together

A hands-on walkthrough for beginners: build one small button, give it structure with HTML, looks with CSS, and behavior with JavaScript, and watch the three layers cooperate on a real page.

Sep 20, 20268 min read
The Frontend Developer Toolkit cover with editor, browser, and CLI motifs
Frontend / Fundamentals

The Frontend Developer Toolkit

A friendly tour of the everyday tools a frontend developer relies on — code editor, browser and DevTools, terminal, version control, package managers, and a dev server — and what each one is actually for.

Sep 20, 202610 min read
Cover illustration for What Frontend Developers Do
Frontend / Fundamentals

What Frontend Developers Do: A Beginner's Guide to the Role

A clear, beginner-friendly look at what frontend developers actually do every day: turning designs into working interfaces, building reusable UI, and caring about responsiveness, accessibility, and speed.

Sep 20, 202610 min read