Skip to Content
JavaScript Intermediate: Write Code That Doesn't Fall Apart When the Data Changes
Intermediate
0 likes
Courses
JavaScript Intermediate: Write Code That Doesn't Fall Apart When the Data Changes

try/catch and Custom Errors

Use try, catch and finally, catch only what you can handle, write your own error classes, and catch errors from async code. After this, expected problems get handled and real bugs still show up.

Course content

Sign in to save your progress.

Course Home

Skip ahead: If you already write your own error classes and check them with instanceof, skip to "Custom error classes".

Errors aren't the enemy. An error that stops the program with a clear message is the second-best outcome after no error. The worst outcome is an error that gets swallowed, so the program keeps going with bad data and nobody knows. try/catch is how you decide which errors you can actually handle, and custom errors are how you tell them apart.

try, catch, finally

function parseQuantity(text) {
  const qty = Number(text);
  if (!Number.isInteger(qty) || qty < 1) {
    throw new RangeError(`Quantity must be a whole number of at least 1, got "${text}"`);
  }
  return qty;
}

for (const input of ["3", "abc"]) {
  try {
    console.log("quantity:", parseQuantity(input));
  } catch (error) {
    console.log(`${error.name}: ${error.message}`);
  } finally {
    console.log("checked", input);
  }
}
quantity: 3
checked 3
RangeError: Quantity must be a whole number of at least 1, got "abc"
checked abc
  • try runs the code.
  • catch runs only if something in try threw. error is whatever was thrown.
  • finally runs either way. Use it for cleanup: hide a spinner, close a file, re-enable a button.

Always throw an Error (or a subclass), never a string. throw "bad quantity" has no stack trace and no name, so whoever catches it can't tell where it came from.

Catch only what you can handle

Here's the pattern that hides bugs:

try {
  saveOrder(order);
} catch (error) {
  // ignore
}

If saveOrder fails because the network is down, fine, maybe you can retry. But if it fails because of a typo in your code, this hides it too. The order isn't saved, the user thinks it is, and the logs are empty.

Ask two questions before writing a catch:

  1. Which errors do I expect here?
  2. What will I actually do about them: retry, show a message, use a default?

If the answer to the second question is "nothing", don't catch. Let it go up to code that can do something, or let it crash loudly.

⚠ Warning: Never leave a catch block empty. At the very least log the error with context: console.error("saving order 1042 failed", error). An empty catch turns a five-minute bug into a two-day investigation.

Custom error classes

To handle some errors and let the rest through, you need to tell them apart. Checking the message text is fragile, since someone will reword it. Make your own error class instead:

class ValidationError extends Error {
  constructor(field, message) {
    super(message);
    this.name = "ValidationError";
    this.field = field;
  }
}

function checkOrder(order) {
  if (!order.email?.includes("@")) {
    throw new ValidationError("email", "Enter a valid email address");
  }
  if (order.items.length === 0) {
    throw new ValidationError("items", "Add at least one item");
  }
}

function submit(order) {
  try {
    checkOrder(order);
    console.log("order OK");
  } catch (error) {
    if (error instanceof ValidationError) {
      console.log(`Show next to ${error.field}: ${error.message}`);
    } else {
      throw error;
    }
  }
}

submit({ email: "ana@example.com", items: ["rice"] });
submit({ email: "ana", items: ["rice"] });
submit({ email: "ana@example.com", items: [] });
order OK
Show next to email: Enter a valid email address
Show next to items: Add at least one item

The important line is throw error in the else. Validation errors are expected, so they're handled. Anything else is a real bug, so it's thrown again instead of being treated as bad input.

  • extends Error gives you the message and the stack trace.
  • this.name makes logs show ValidationError instead of Error.
  • Extra fields like field carry what the handler needs.

The tutorial Custom Error Classes You Can Actually Catch adds an HTTP error class and the cause option for wrapping errors.

Errors in async code

try/catch works around await exactly like synchronous code. It does not catch errors from a promise you didn't await:

async function failLater() {
  throw new Error("failed after waiting");
}

try {
  failLater();
} catch (error) {
  console.log("never runs");
}
console.log("try block finished");
try block finished
...
Error: failed after waiting
...

The try finished before the promise rejected, so the catch never saw it. Node printed the error and stopped the program with exit code 1. Add await in front of failLater() and the catch gets it.

💡 Tip: When you rethrow with more context, keep the original: throw new Error("Could not save order 1042", { cause: error }). Node and the browser console show both, so you don't lose the real reason.

Common mistakes

  • Empty catch blocks.
  • Catching everything and showing "Something went wrong" for a typo in your own code.
  • Throwing strings.
  • Checking error.message === "..." instead of the error class.
  • Forgetting await inside try.

What happens if you skip this

Your app will either crash on every bad input, or, after someone "fixes" that with a big try/catch, silently do the wrong thing. The second is worse. Crashes get reported. Silent failures get discovered by customers.

Find one catch block in your code. If it caught a TypeError from a typo, would anyone ever find out?

About

Use try, catch and finally, catch only what you can handle, write your own error classes, and catch errors from async code. After this, expected problems get handled and real bugs still show up.

Duration
5 min
Last update
09/08/2026
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.