Module 7 · Error Handling, Debugging & Testing ⏱ 19 min

Throwing Custom Errors

By the end of this lesson you will be able to:
  • Throw an Error with a descriptive message to fail fast on invalid input
  • Choose the right built-in error subclass, such as TypeError or RangeError
  • Define a custom error class and filter errors in a catch with instanceof

Every program gets bad data eventually: a negative age, a missing field, a string where a number belonged. The question isn't how to prevent it — you can't — but what your code does when it happens. One option is to soldier on, returning NaN, undefined, or an empty string and hoping the caller notices. That hope is rarely rewarded; the bad value travels silently downstream and breaks something ten functions away, with no clue where it went wrong.

The other option is to throw: stop the current operation right now and announce, loudly and with a message, exactly what was wrong. throw hands control back to the nearest willing catch, or crashes if there isn't one — but either way the failure is visible and located, not a quiet poison in your data.

flowchart TD
  T["throw new Error(msg)"] --> S["unwind the call stack"]
  S --> Q{"enclosing try/catch?"}
  Q -- Yes --> C["catch handles it"]
  Q -- No --> X["program crashes"]
  style C fill:#1e7f3d,color:#fff
  style X fill:#b91c1c,color:#fff
throw searches outward for the nearest enclosing try...catch; if none exists, the program crashes.

Throw an Error, with a message

The statement is just the word throw followed by a value. Convention — and good sense — says that value should be an Error object, created with new Error("..."). The string you pass becomes its message, the human-readable description of what went wrong, and JavaScript automatically attaches a stack trace recording where it was thrown.

throw new Error("insufficient funds");

The moment throw runs, the current line is abandoned. JavaScript unwinds outward, function by function, looking for an enclosing try...catch. Find one, and that catch runs with your error in hand; find none, and the program halts with your message printed. Either way, nothing after the throw in the same block executes.

withdraw throws when the amount exceeds the balance; the caller catches it. Press Run.
function withdraw(balance, amount) {
  if (amount > balance) {
    throw new Error("insufficient funds");
  }
  return balance - amount;
}
try {
  console.log(withdraw(100, 30));
  console.log(withdraw(100, 500));
} catch (e) {
  console.log("declined: " + e.message);
}

Throw early, fail loud

The disciplined way to use throw is to guard a function's preconditions at the very top: the things that must be true for the rest of the body to make sense. A function that divides by its argument can refuse a zero immediately rather than returning Infinity. A function that looks up a record can refuse a missing id rather than returning undefined and letting the caller crash three lines later with a confusing 'cannot read property of undefined'.

This is called failing fast. A failure caught at the door comes with a clear message and a precise location; the same failure deferred propagates into garbage that breaks something unrelated. When in doubt, throw at the boundary — the first line that receives untrusted input.

flowchart TD
  E["Error<br/>the base class"] --> T["TypeError"]
  E --> R["RangeError"]
  E --> Y["SyntaxError"]
  E --> M["YourError extends Error"]
  style E fill:#3776ab,color:#fff
  style M fill:#1e7f3d,color:#fff
Every built-in error is a subclass of Error — and so are your custom ones, which is why instanceof Error still matches them.

Use the right kind of error

Error is the generic base, but JavaScript ships more specific subclasses that name the nature of the mistake — and you should reach for them, because that name is exactly what a downstream catch will read. TypeError means a value is the wrong type (calling a method on undefined, adding a number to an object). RangeError means a value is the right type but out of the allowed range (a negative array length, an array index past the end). SyntaxError is what JSON.parse throws for malformed text.

Throwing new RangeError("index out of bounds") instead of a plain Error tells the next layer precisely what family of problem this is, so it can choose to catch only that kind.

Pick the subclass that matches the mistake. catch reads .name to learn the family. Press Run.
function nth(items, i) {
  if (!Array.isArray(items)) throw new TypeError("expected an array");
  if (i < 0 || i >= items.length) throw new RangeError("index out of bounds");
  return items[i];
}
console.log(nth(["a", "b", "c"], 1));
try {
  nth(["a", "b", "c"], 9);
} catch (e) {
  console.log(e.name + ": " + e.message);
}

Your own error classes

When the built-in names don't fit, you can create your own. A custom error class extends Error, passes the message up to the base with super(message), and sets its own this.name. Now callers can distinguish your error from any other with instanceof — far more reliable than matching on a message string, which a single typo can break.

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

Because it inherits from Error, an InvalidAgeError is still an Error — so a generic catch sees it too. That extends chain is what lets you be specific without losing the safety net of a broad handler.

A custom error is still an Error: instanceof matches both. Press Run.
class InvalidAgeError extends Error {
  constructor(message) {
    super(message);
    this.name = "InvalidAgeError";
  }
}
function setAge(age) {
  if (typeof age !== "number" || age < 0) {
    throw new InvalidAgeError("age must be a non-negative number");
  }
  return age;
}
try {
  setAge(-5);
} catch (e) {
  console.log(e instanceof InvalidAgeError);
  console.log(e instanceof Error);
  console.log(e.name + ": " + e.message);
}
flowchart TD
  C["catch (e)"] --> Q["e instanceof RangeError?"]
  Q -- Yes --> H["handle the range case"]
  Q -- No --> RE["throw e<br/>let it propagate"]
  style H fill:#1e7f3d,color:#fff
  style RE fill:#b45309,color:#fff
Since catch grabs everything, filter with instanceof and re-throw what you don't handle.

Handle one kind, re-throw the rest

A JavaScript catch is type-agnostic: it grabs every thrown value, regardless of kind. So when you want to handle only a specific error and let everything else keep propagating, you filter inside the catch with instanceof. If the error is the kind you expected, handle it; otherwise, throw e to put it back in flight for an outer handler.

try {
  risky();
} catch (e) {
  if (e instanceof RangeError) {
    console.log("handled a range issue");
  } else {
    throw e;
  }
}

Re-throwing preserves the original error and its stack, so the failure stays honest all the way up the stack.

Exercise

What does this print? half throws for one of these inputs — trace which calls actually run.

function half(n) {
  if (n === 0) throw new Error("no zero");
  return 10 / n;
}
try {
  console.log(half(5));
  console.log(half(0));
  console.log(half(2));
} catch (e) {
  console.log("caught: " + e.message);
}
Exercise

Write factorial(n) that returns n! (so factorial(5) is 120) for n >= 0, and throws new Error("negative") when n < 0.

function factorial(n) {
  // throw new Error("negative") for n < 0; otherwise return n!
}
Exercise

This findUser(id) throws a bare string instead of a real Error. That loses the stack trace and means a caller's err.message comes back undefined. Change the throw so that what is thrown is instanceof Error and carries .message === "no user".

function findUser(id) {
  if (id < 0) throw "no user";
  return "user-" + id;
}
Exercise

The custom error class BadEmailError is written for you. Complete validateEmail(s): if s does not include an @, throw new BadEmailError("missing @"); otherwise return true.

class BadEmailError extends Error {
  constructor(message) {
    super(message);
    this.name = "BadEmailError";
  }
}
function validateEmail(s) {
  // throw BadEmailError if s has no "@", otherwise return true
}
Exercise

A catch block grabs an error. How do you handle only RangeErrors and let every other error keep propagating?

Recap

  • throw new Error("message") creates an error and unwinds the call until a catch handles it, or the program crashes.
  • Fail fast: guard preconditions at the top of a function rather than returning NaN or garbage that breaks code far away.
  • Pick the right built-in subclassTypeError for wrong types, RangeError for out-of-range values.
  • A custom error class extends Error, calls super(message), and sets this.name; instanceof Error still matches it.
  • Filter inside one catch with instanceof and re-throw (throw e) the kinds you don't handle.
  • Always throw an Error, never a bare string — you'd lose the stack trace and the .message.

Next you'll step back from writing code to fixing it — systematic debugging strategies for when something goes wrong and you have to find out what.

Checkpoint quiz

Why prefer throw new Error("bad") over throw "bad"?

You write class MyErr extends Error. After throw new MyErr("x"), what is e instanceof Error in the catch?

Go deeper — technical resources