Engrain by ByteRush

JS.13.01 Mid-level· 10 minCore — asked often

Error types, cause and custom errors: errors that tell you what went wrong

The built-in error types, wrapping errors with cause, writing custom error classes that work with instanceof, AggregateError, Error.isError, and why you never throw strings.

Comes up in: JavaScript, Debugging

Learn#

Irfan’s error tracker shows 4,000 copies of “Something went wrong” and nothing else. Every layer of his app caught errors and threw a new message, so the real cause was lost.

JavaScript has built-in error types: TypeError (wrong kind of value), RangeError (number out of range), SyntaxError (bad code or bad JSON), ReferenceError (unknown variable), and a few more.

When you add context, keep the original as the error cause:

class PaymentError extends Error {
  constructor(message, options) {
    super(message, options);
    this.name = "PaymentError";
  }
}
function charge() {
  try {
    JSON.parse("{ bad json");
  } catch (err) {
    throw new PaymentError("Could not read the bank's reply", { cause: err });
  }
}
try { charge(); } catch (e) {
  console.log(e instanceof PaymentError, e instanceof Error, e.name);
  console.log(e.message, "← because:", e.cause.name);
}

Output (verified)

true true PaymentError
Could not read the bank's reply ← because: SyntaxError

PaymentError is a custom error: a class that extends Error, so callers can test for it with instanceof and handle it on its own.

Wrapping a low-level error without losing itStep 1 of 3

Call stack

  • charge
  • JSON.parse

State

  • err: SyntaxError

Console

    empty

    JSON.parse fails deep inside charge() and throws a SyntaxError.

    Read the steps as text
    1. JSON.parse fails deep inside charge() and throws a SyntaxError.
    2. charge() catches it and throws a PaymentError with a clear message. The SyntaxError goes in cause.
    3. The caller checks instanceof PaymentError and shows a payment message. The log keeps both errors and both stacks.

    Predict#

    Predict first

    Code runs throw 'card declined'. What does the catch block's e.stack give?

    Interview angle#

    Say it like this — about 30 seconds

    “I always throw Error objects, never strings, so I keep a stack trace. When a lower layer fails, I wrap it in an error that explains what the user was doing, and pass the original as cause. For errors callers must handle differently, I write a class that extends Error, calls super with the message and options, and sets name. Then callers use instanceof. AggregateError holds several errors at once, for example from Promise.any.”

    Stays on your device. Nothing is uploaded.
    Follow-up How do you create a custom error class correctly?
    Extend Error, call super(message, options) to keep cause, set this.name, and add fields such as status.
    Follow-up Why set this.name?
    The name appears in logs and stack traces; without it every custom error prints as plain “Error”.
    Follow-up When is instanceof Error not enough?
    For errors made in another window, such as an iframe: each window has its own Error class. Error.isError(value) checks for a real error object.

    Practice#

    Guided

    Write an HttpError class, then wrap a failed request in a clear error that keeps its cause.

    Done when

    • new HttpError(404, "Not found") has the right name, status and message.
    • It passes instanceof HttpError and instanceof Error, and has a stack.
    • loadProfile rejects with a new message and the original error as cause.
    Your solution
    Ctrl/⌘ + Enter

    Remember#

    Try to answer each card before you open it. Then add them to your reviews.

    How do you attach the original error when throwing a new one?

    new Error(message, { cause: originalError })

    Why never throw a string?

    Strings have no stack trace and no name.

    What three things does a good custom error class do?

    Extends Error, calls super(message, options), sets this.name.

    What does AggregateError hold?

    Several errors at once, in its errors array.

    Go deeper

    Four layers. Stop at the one your interview needs; each ends with a quick check.

    The core ideaEvery round

    TypeError is the most common: reading a property of null, or calling something that is not a function. RangeError comes from things like new Array(-1) or a stack overflow. Check error.name or instanceof to tell them apart.

    Check yourself: Which error does null.x throw?

    TypeError.

    How it works underneathSenior rounds

    super(message, options) lets the Error constructor set message, stack and cause Source: MDN: Error: cause. Class syntax keeps the prototype chain right, so instanceof works; old ES5 transpile targets broke this.

    Check yourself: Why pass options to super() in a custom error?

    So the cause option is stored on the error.

    Edge cases and trapsWhere people slip
    AggregateError holds many errors in .errors; Promise.any rejects with one.
    Error.isError(value) is new in ES2026: Chrome 134, Firefox 138, Node.js 24.3.0; not yet in Safari.
    Anything can be thrown, so always check before reading error.message.
    Check yourself: Is Error.isError available in every browser today?

    No. Check support and feature-detect it.

    In productionStaff-level answers

    Show users a short, helpful message; send the full error, its cause chain and stack to your error tracker (JS.13.02). Most trackers show causes as linked errors Source: ECMA-262: InstallErrorCause.

    Check yourself: What should an error shown to users not contain?

    Internal details such as stack traces, queries or secrets.

    Finished the five steps? Mark it, and its cards join your reviews.

    Last checked 2026-10-31. Spot a mistake? Use “Report an error” below — fixes are listed on the corrections page.

    Interview questions this answers

    • How do you create a custom error class correctly?