cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Throwing & custom errors

Learn how throw stops normal flow, why custom Error classes make failures easier to handle, how error.cause preserves original failures, and when AggregateError reports many problems at once.

By the end, you can
  • 01
    Throw with intentExplain why Error objects are better caught values than strings or plain objects.
  • 02
    Create custom errorsExtend Error, set a reliable name, and add fields such as field or statusCode.
  • 03
    Preserve contextWrap low-level failures with cause and bundle several failures with AggregateError.

When failure needs a clear signal

The previous try, catch & finally lesson showed how to catch a runtime problem and clean up afterward. This lesson focuses on the other side: how to create a failure signal that is specific, catchable, and useful when somebody else receives it.

JavaScript uses throw for failures that should not continue as normal results. You can throw any value, but professional code usually throws Error objects or custom subclasses of Error. That gives catch blocks a predictable shape: name, message, often stack, sometimes cause, and any extra fields your application needs.

Real-life analogythrow is pulling the fire alarm

A generic alarm only says something is wrong. A smoke alarm says there is a fire. Custom errors are those specific alarm types: responders know what happened and which response fits.

In real life: A fire alarm is pulled
In JavaScript: A throw statement runs
In real life: Normal work in the building stops
In JavaScript: Normal statement-by-statement flow stops
In real life: People leave floors until the fire marshal takes over
In JavaScript: The call stack unwinds until a catch handles it
In real life: A specific alarm says smoke is in the kitchen
In JavaScript: A custom class says ValidationError or ConfigError

Where the analogy stops: A building alarm is heard by everyone at once. JavaScript errors move along the current call stack; code in unrelated work does not automatically handle them.

We will cover throw, extending Error, error.cause, wrapping errors, and AggregateError, in that order.

throw stops normal flow

STEP THROUGH

A throw statement can throw any JavaScript value. That fact is useful to know because you may catch older code that throws strings or objects. But “legal” does not mean “pleasant to handle.” Step through the experiment, switch the thrown value, and compare the real caught values.

Throw different values and catch the real result
Step 0 of 6Ready
Your turn: follow the blue line

Throwing anything is legal, but throwing Error objects gives catch blocks a predictable shape.

Running in
  1. script
Next: line 8
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
function throwValue(kind) {  if (kind === "text") throw "plain text";  if (kind === "number") throw 42;  if (kind === "object") throw { code: "E_BOX" };  throw new Error("File is missing");}   throwValue("error");} catch (caught) {  console.log(typeof caught);  console.log(caught instanceof Error);  console.log(caught instanceof Error ? caught.message : typeof caught === "object" ? JSON.stringify(caught) : caught);}
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Choose what line 8 will ultimately throw

Changing the value starts a fresh replay. Predict typeof caught and caught instanceof Error first.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Notice the important difference: throw "plain text" is caught as text, and throw 42 is caught as a number. Only throw new Error("File is missing") gives the catch block a standard error object.

Stack traces are useful, but engine-specific

Most browsers and JavaScript runtimes attach a stack string to Error objects. In V8-based engines, a custom subclass stack heading usually includes the subclass name when you set this.name. The exact stack format is not standardized. Error.captureStackTrace is also V8-only, so treat it as an environment feature, not portable JavaScript.

Extending Error

INTERACTIVE

A custom error class is a named category of failure. The class does not make the program fail harder; it makes the failure easier to recognize. Since you have already learned class inheritance with extends and super, the pattern is small:

  • Extend Error.
  • Call super(message) before using this.
  • Set a readable name, often with a fixed string or static class field.
  • Add stable fields, such as field or statusCode.

The playground uses AppError as the base class and ValidationError as the specific class. The catch block checks the most specific type first.

Names and production builds

You may see the idiom this.name = new.target.name. It works in unminified JavaScript, but production minifiers can rename classes. If logs or UI rely on the name, set it explicitly, as this lesson does.

Dispatch a custom error hierarchy
Step 0 of 5Ready
Your turn: follow the blue line

Custom error classes turn a generic failure into a specific signal a catch block can route.

Running in
  1. script
Next: line 21
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
class AppError extends Error { static errorName = "AppError";  constructor(message, options) {    super(message, options);    this.name = this.constructor.errorName;    this.statusCode = options?.statusCode ?? 500;  }} class ValidationError extends AppError { static errorName = "ValidationError";  constructor(field, message) {    super(message, { statusCode: 400 });    this.field = field;  }} function saveUser(input) {  if (!input.email) throw new ValidationError("email", "Email is required");  return "saved";}   saveUser({});} catch (error) {  if (error instanceof ValidationError) {    console.log("validation", error.field, error.statusCode);  } else if (error instanceof AppError) {    console.log("app", error.statusCode);  }}
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

instanceof follows the prototype chain, not the text in error.name. The name is for humans and logs. The class hierarchy is for reliable dispatch.

error.cause: keep the original incident report

Sometimes the error you want to show at this level is not the original error. A JSON parser might throw SyntaxError, but your app was trying to load a settings file. The outer message should say what your app was doing. The original parser problem should still be attached.

Real-life analogycause is an attached incident report

The summary report is easier for the team to understand, but the original report still matters when somebody investigates.

In real life: A manager writes a summary report
In JavaScript: The outer ConfigError message
In real life: The first responder's original notes are stapled behind it
In JavaScript: The original SyntaxError in error.cause
In real life: An investigator reads each report in order
In JavaScript: A helper walks the cause chain

Where the analogy stops: A paper report has a neat page order. JavaScript lets cause be any value, so code should check current instanceof Error before following it.

Modern Error constructors accept an options object: new Error("Couldn't load profile", { cause: err }). Your custom subclasses can pass that same options object to super.

Wrapping low-level errors

STEP THROUGH

Wrapping means catching one error and throwing another error that adds the context your layer understands. The wrapper should not erase the original error. It should carry it in cause.

Wrap a low-level error with cause
Step 0 of 6Ready
Your turn: follow the blue line

Wrapping errors preserves both levels: the useful application message and the original technical cause.

Running in
  1. script
Next: line 26
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
class ConfigError extends Error {  constructor(message, options) {    super(message, options);    this.name = "ConfigError";  }} function parseConfig(text) {  try {    return JSON.parse(text);  } catch (error) {    throw new ConfigError("Config file is not valid JSON", { cause: error });  }} function causeChain(error) {  const chain = [];  let current = error;  while (current instanceof Error) {    chain.push(current.name + ": " + current.message);    current = current.cause;  }  return chain;}   parseConfig("{ bad json }");} catch (error) {  console.log(causeChain(error).join(" -> "));}
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

The catch block now receives a ConfigError with a useful application message, while error.cause still points at the original SyntaxError. This gives logs both the “what were we doing?” summary and the “what exactly broke?” detail.

AggregateError: one report, several incidents

INTERACTIVE

cause is a chain: one error happened because another error happened. AggregateError is a bundle: several sibling errors happened during one operation. The built-in class has a summary message and an .errors array.

Bundle several validation failures
AggregateError validationPop out in the code editor (opens in a new tab)JavaScript
const errors = [  new Error("Email is required"),  new Error("Must be 13 or older"),];const problem = new AggregateError(errors, "User has validation errors");console.log(problem.name);console.log(problem.message);console.log(problem.errors.length);
Outputready
  • consoleChoose failures, then run validation.
Try it yourself

Start with two failing rules. Run validation, then turn failures off one at a time and run again.

A direct AggregateError example. Promise.any can also create AggregateError later in the course; here you are constructing one yourself.

Later in the course, Promise combinators show another place you may see AggregateError. Here, you are constructing it directly: an array of failures plus a message.

Designing useful errors

A good error helps two audiences at once: code that branches and humans reading a log. Keep those jobs separate. A message is for humans; stable fields are for code.

Good error design
PieceJobExample
name or classNames the category of failureValidationError
messageExplains the failed rule or operationEmail is required
causeKeeps the lower-level original errorA SyntaxError from JSON.parse
Extra fieldsGive handlers stable factsfield = "email", statusCode = 400

Sort these messages. Good messages say what failed and usually how to fix or classify it. Bad messages make the next developer guess.

Good or bad error message?
  • Email is required
  • Failed
  • Config file is not valid JSON
  • Oops!!!
  • Age must be 13 or older
  • undefined is not valid
Try it yourself
0 of 6 correct

Put each message where it belongs. Imagine finding it in a log at the end of a long day.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common misconceptions

“Anything I can throw is equally good.”

Throwing anything is legal, but strings and numbers lack standard error fields. Prefer Error objects.

“Setting name makes instanceof work.”

instanceof checks prototypes. name is a readable label for logs and stack headings.

“A wrapper should replace the original error.”

A wrapper should summarize and preserve. Put the original in cause.

“Catch the broadest type first.”

Check specific subclasses before broad parents, or the specific handler never gets a chance.

“AggregateError is only for promises.”

Promise APIs can produce it, but new AggregateError(errors, message) is a direct built-in constructor you can use when it fits.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upCreate a ValidationError

Run the starter program. What exact line does it print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
class ValidationError extends Error {
  constructor(field, message) {
    super(message);
    this.name = "ValidationError";
    this.field = field;
  }
}
const error = new ValidationError("email", "Email is required");
console.log(error.name + ":" + error.field + ":" + error.message);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticePredict instanceof dispatch

    Predict the three booleans before running the code.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    class AppError extends Error {}
    class ValidationError extends AppError {}
    const error = new ValidationError("Missing email");
    console.log(error instanceof ValidationError);
    console.log(error instanceof AppError);
    console.log(error instanceof Error);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeWrap with cause

      What two lines print?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      try {
        JSON.parse("{ bad json }");
      } catch (error) {
        const wrapped = new Error("Could not load profile", { cause: error });
        console.log(wrapped.message);
        console.log(wrapped.cause.name);
      }

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticePrint a cause chain

        What is the first line printed by the loop?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const root = new TypeError("Token must be text");
        const middle = new Error("Could not read settings", { cause: root });
        const top = new Error("Could not start app", { cause: middle });
        let current = top;
        while (current) {
          console.log(current.name + ": " + current.message);
          current = current.cause;
        }

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeBundle multiple validation failures

          What name and length print?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          const errors = [new Error("Email is required"), new Error("Age must be 13 or older")];
          const problem = new AggregateError(errors, "Profile has validation errors");
          console.log(problem.name);
          console.log(problem.errors.length);

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            7 QUESTIONS
            Lesson quiz · 7 questionsScore: first tries count
            1. Question 1 of 7What does throw do when it runs?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does the Error instanceof snippet print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              try {
                throw new Error("No token");
              } catch (error) {
                console.log(error instanceof Error);
              }

              Choose an answer to see the explanation.

            3. Question 3 of 7Why should a custom error class set a fixed name (or a carefully controlled equivalent)?

              Choose an answer to see the explanation.

            4. Question 4 of 7What does the subclass instanceof AppError snippet print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              class AppError extends Error {}
              class ValidationError extends AppError {}
              const problem = new ValidationError("Bad email");
              console.log(problem instanceof AppError);

              Choose an answer to see the explanation.

            5. Question 5 of 7What belongs in error.cause?

              Choose an answer to see the explanation.

            6. Question 6 of 7What does the AggregateError snippet print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const one = new Error("One");
              const two = new Error("Two");
              const all = new AggregateError([one, two], "Both failed");
              console.log(all.name);
              console.log(all.errors.length);

              Choose an answer to see the explanation.

            7. Question 7 of 7Which catch order is safest for a hierarchy?

              Choose an answer to see the explanation.

            Key takeaways

            • throw stops normal flow until a catch takes responsibility.
            • Throw Error objects so handlers get name, message, stack information, and instanceof.
            • Custom subclasses such as ValidationError make catch blocks precise.
            • error.cause preserves the lower-level incident behind a higher-level summary.
            • AggregateError reports several sibling failures in one object.

            Final definition: Throwing is JavaScript’s failure path; custom errors give that path a specific type, useful data, and preserved context.

            Up next: Error handling strategies.

            CompleteFrontend Clear concepts. Working examples.