Command Palette

Search for a command to run...

Hectal
PHASE 13Intermediate ~22 min· topic 3 of 6

Topic 13.3

Throwing Your Own Errors

In one line

throw new Error("message") stops a function and hands a problem to its caller; good errors use the right type, a message that says what was wrong and what was expected, your own Error subclasses for problems callers handle differently, and the ES2022 cause option to keep the original error.

Think of it like this

A lift that refuses to move. If too many people get in, a good lift doesn't creak upward and get stuck between floors. It stops at once, beeps, and shows a clear sign: "Overload: maximum 8 people, please step out". Throwing an error is your function doing the same: refusing to carry on with bad input, right away, with a message that tells people what to do.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

throw statement
throw value; stops the function and sends the value up to the nearest catch.
Error subclass
Your own error type made with class MyError extends Error, so callers can recognise it.
super(message)
Inside a subclass constructor, the call that lets the parent Error set up message and stack.
cause
An option (ES2022) that attaches the original error to a new one: new Error(msg, { cause: err }).
Wrapping an error
Catching a low-level error and throwing a clearer one that keeps the original as its cause.
Fail fast
Check input at the start and throw at once, so the error points straight at the real problem.
Error code
A short fixed label on an error, like "OUT_OF_STOCK", that code can check reliably.

Step by step

01Throw at the first sign of bad input

Check the arguments first, before doing any work. Each check either passes or throws with a precise message.

cart.jswhole filejavascript
function setQuantity(qty) {
  if (!Number.isInteger(qty)) {
    throw new TypeError("Quantity must be a whole number, got " + JSON.stringify(qty));
  }
  if (qty < 1 || qty > 10) {
    throw new RangeError("Quantity must be from 1 to 10, got " + qty);
  }
  return qty;
}

02Write messages for the person fixing it

Compare the two messages below. The second one could be fixed by someone who has never seen the code.

messages.jswhole filejavascript
// Unhelpful
throw new Error("Invalid input");

// Helpful: what, the value, what was expected
throw new RangeError("Discount must be between 0 and 100 percent, got 150");

03Make your own error types

A base class sets the name once for every subclass. this.constructor.name is the name of the class that was actually created with new.

Each subclass can add data the caller needs, like which form field was wrong.

errors.jswhole filejavascript
class AppError extends Error {
  constructor(message, options) {
    super(message, options);       // sets message, stack (and cause)
    this.name = this.constructor.name; // "ValidationError", "NotFoundError"...
  }
}

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

class NotFoundError extends AppError {}

04Let callers react by type

The caller decides what each kind of error means for the user. Anything it doesn't recognise is rethrown, as in Topic 13.2.

Let callers react by typediagram
Rendering diagram…

05Wrap low-level errors with cause

The user should read "Could not load your settings". The developer needs the original SyntaxError. cause keeps both. DevTools and Node.js print the cause under the error.

settings.jswhole filejavascript
function loadSettings(text) {
  try {
    return JSON.parse(text);
  } catch (err) {
    throw new Error("Could not load your settings", { cause: err });
  }
}

Try it yourself

  1. 1

    Add a new rule

    In the first example, make setQuantity also reject quantities that are odd numbers, with a helpful RangeError message. Add 5 to the input list. Predict the output first.

  2. 2

    Forget to set the name

    In the second example, delete this.name = this.constructor.name;. Predict what the last line prints now. Why does the name matter for anyone reading logs?

  3. 3

    Add an error code

    In the second example, give NotFoundError a code property with the value "NOT_FOUND" and print err.code in the caller. Codes stay fixed even if you reword the message.

Code & diagrams

throw stops the function and reaches the caller New tab

`JSON.stringify` puts quotes around strings, so the message shows that `"2"` was text, not the number 2.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

(this line only runs for good input)
OK: 3
RangeError: Quantity must be from 1 to 10, got 0
TypeError: Quantity must be a whole number, got "2"
TypeError: Quantity must be a whole number, got 2.5
Your own error classes ES2015+ New tab

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Renamed to Asha K
Fix the field name: Name can't be empty
Not found: No user with id 9
ValidationError: Email needs an @ | instanceof Error: true
Wrapping an error with cause ES2022+ New tab

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

For the user: Could not load your settings
For the developer: SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2)
cause is a SyntaxError? true
Why you throw Error objects, not strings New tab

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

throwsString | type: string | instanceof Error: false | has a stack: false
throwsError | type: object | instanceof Error: true | has a stack: true

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Forget `new` and `throw`

Inside a check, write Error("Quantity too big"); instead of throw new Error("Quantity too big");.

terminal
$ Console
── what you'll see ──
No error appears, and the bad quantity is accepted.

Break #2

Use `this` before `super` in a subclass

In ValidationError's constructor, write this.field = field; before super(message);.

terminal
$ Console
── what you'll see ──
Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

Myth vs fact

Myth

Throwing a string is fine; it's still an error.

Fact

A string has no stack trace, no name, and fails instanceof Error. Error-reporting tools can't tell you where it came from. Always throw an Error object.

Myth

Every problem should be an error.

Fact

Expected outcomes, like "no search results" or "the user cancelled", are normal values. Use errors for situations the function can't handle.

Myth

Wrapping an error loses the original details.

Fact

Not with cause: the original error, with its own stack, stays attached as err.cause (ES2022).

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Subclassing Error works correctly with native ES2015 classes. Code compiled down to ES5 (old Babel or TypeScript targets) used to break instanceof for subclasses, which needed Object.setPrototypeOf(this, new.target.prototype) in the constructor. Modern targets don't need it.

  • ▸

    Error.captureStackTrace(this, SomeFunction) (V8 and recent JavaScriptCore) sets a stack that starts below SomeFunction, hiding helper frames. Node.js uses it so errors from its own helpers point at your code.

  • ▸

    The second argument of Error is an options object; cause is its only standard option so far, and it works on every built-in error type and on AggregateError (new AggregateError(errors, message, { cause })). Node.js prints the cause chain as [cause]: ...; DevTools shows Caused by:.

  • ▸

    Promise rejections should use Error objects too: reject(new Error("...")), and throw inside an async function becomes a rejection. Rejecting with strings makes unhandledrejection reports useless.

Remember this

  1. 1

    The **throw statement** stops the current function at once and sends a value up the call stack until a catch receives it (Topic 13.2). You can throw anything, but always throw an Error object: throw new Error("..."). Only Error objects carry a stack (where it happened), work with instanceof, and print usefully in consoles and error-reporting tools. throw "oops" throws a bare string with none of that.

  2. 2

    Pick the built-in type that matches the problem, so callers can tell problems apart: TypeError when an argument is the wrong kind ("5" where a number was needed), RangeError when a number is outside the allowed range (a quantity of 0 or 500), and plain Error for everything else. You call them all the same way: new RangeError("message").

  3. 3

    A message that helps answers three questions: what failed, what value caused it, and what was expected. "Invalid input" makes the reader start hunting. "Quantity must be a whole number from 1 to 10, got 0" tells them exactly what to fix. Include the bad value (use JSON.stringify(value) so "2" and 2 look different), but never passwords, tokens or other private data, because error messages end up in logs and on screens.

  4. 4

    When callers need to react differently to your own kinds of problem (show a field message for bad input, show "not found" for a missing item), make Error subclasses with class ValidationError extends Error (classes and extends are ES2015, Topic 7.7). Call super(message), set this.name so the console prints the right name, and add extra properties like field or a short machine-readable code. Callers then use err instanceof ValidationError instead of reading the message text.

  5. 5

    Often you catch a low-level error and want to throw a clearer, higher-level one: "Could not load your settings" instead of "Unexpected token". Since ES2022, new Error(message, { cause: err }) attaches the original error as err.cause, so the friendly message and the technical detail are both kept. This is called wrapping an error. Before cause, people lost the original error or glued its message into the new one.

  6. 6

    Throw at the boundary where the bad value first arrives: the start of a function that receives input. That's called failing fast: the error points straight at the cause, instead of a confusing TypeError three functions later. For problems that are a normal outcome (a search with no results), return a value like null or an empty array instead; errors are for things that shouldn't happen in a normal run.

Explain it without notes

01

Why should you always throw Error objects rather than strings?

02

How do you write a custom error class, and why would you?

03

What does the cause option do, and when do you use it?

04

What makes an error message helpful?

Practice

01

Write percent(part, whole) that throws a TypeError if either argument isn't a number, a RangeError if whole is 0, and otherwise returns the rounded percentage. Call it with (1, 4) and (1, 0) and print the results or the errors.

02

Make an OutOfStockError class with an item property and the name OutOfStockError. Throw it from buy(item, stock) when stock[item] is 0, catch it, and print Sorry, <item> is sold out.

Trade-offs

  • ↔

    Custom error classes make handling precise, but each one is more code to keep; a single class with a code property ("NOT_FOUND", "INVALID") is a lighter alternative for many small cases.

  • ↔

    Throwing forces callers to deal with a failure; returning null or a result object is lighter but easier to ignore by mistake.

Done when you can

  • I always throw Error objects, never strings.

  • I choose TypeError, RangeError or Error to match the problem.

  • I write messages that say what failed, the bad value and what was expected.

  • I can write an Error subclass with a correct name and extra properties.

  • I can wrap an error with { cause } and read err.cause.