completefrontend
Open lab My progress
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 the right thingExplain why an Error object is easier to catch and handle than text or a plain object.
  • 02
    Make your own errorsWrite a class that extends Error, set its name, and add properties like field or statusCode.
  • 03
    Keep the detailsKeep the original error in cause when you wrap it, and put several errors in one AggregateError.

When something goes wrong, say so clearly

In the try, catch & finally lesson, you learned how to catch an error and clean up afterward. This lesson is about the other side: how to create an error. A good error says exactly what went wrong, so the code that catches it knows what to do.

JavaScript uses throw when something goes wrong and the code can't go on as normal. You can throw any value. But most code throws an Error object, or an object made from your own class based on Error. Then every caught error has the same parts:

  • name: what kind of error it is, like ValidationError.
  • message: a short sentence that says what went wrong.
  • stack: in most places, text that lists the lines that led to the error.
  • cause: sometimes, an earlier error that led to this one.
  • Any extra properties your app needs, like field.
Real-life analogythrow is pulling the fire alarm

A plain alarm only says that something is wrong. A smoke alarm says there is smoke in the kitchen. Your own error classes are like that smoke alarm. The people who respond know what happened and what to do.

In real life: Someone pulls the fire alarm
In JavaScript: A throw statement runs
In real life: Normal work in the building stops
In JavaScript: The next lines of code don't run
In real life: People leave floor by floor until they reach the fire warden
In JavaScript: JavaScript leaves each function until it finds a catch
In real life: A smoke alarm says there is smoke in the kitchen
In JavaScript: A class like ValidationError or ConfigError says what went wrong

Here is a first look at an error with all of these parts. Don't worry about cause yet; you'll learn it later in this lesson.

An error with a cause, turned into JSON textPop out in the code editor (opens in a new tab)JavaScript
const original = new SyntaxError("Bad JSON");
const error = new Error("Could not load settings", { cause: original });
error.field = "settings";
console.log(error.cause.name);
console.log(JSON.stringify(error));
console.log(JSON.stringify({
  name: error.name,
  message: error.message,
  cause: error.cause.name,
  field: error.field,
}));
  • Line 1 makes a SyntaxError. That is the kind of error JSON.parse throws for broken text.
  • Line 2 makes a new error and keeps the first one as its cause.
  • Line 3 adds an extra property, field.
  • Line 4 reads the first error back through error.cause. It prints SyntaxError.
  • Line 5 turns the error into JSON text and prints {"field":"settings"}.

Why only field? JSON.stringify copies only the properties that are marked as listable. On an error, name, message, and cause are not listable. The field you added is.

Lines 6 to 11 build a plain object by hand, so all four parts get copied. Do this when a log or a server reply needs the whole error.

A newer check, Error.isError, is not available everywhere yet. For example, Node 22 doesn't have it. So this lesson doesn't use it.

This lesson covers these, in order:

  • throw
  • your own error classes, made with extends Error
  • error.cause
  • wrapping errors
  • AggregateError

throw stops the code

STEP THROUGH

A throw statement can throw any value: text, a number, an object, or an Error. This is good to know, because older code sometimes throws text or plain objects. But a value you are allowed to throw is not always easy to handle. First, see what throw does to the lines after it.

Code after throw does not runPop out in the code editor (opens in a new tab)JavaScript
console.log("before");
throw new Error("No token");
console.log("after");

Line 1 runs and prints before. Line 2 makes an Error and throws it. JavaScript stops right there, so line 3 never runs. If a catch is around this code, the catch takes over. If not, JavaScript shows an "uncaught" error in the console.

Three lines of code. Line 1, console.log before, runs and prints before. Line 2, throw new Error No token, throws, and the code stops there. Line 3, console.log after, never runs.console.log("before")runs: prints beforethrow new Error("No token")throws: the code stops hereconsole.log("after")never runs
Line 1 prints before. Line 2 throws, so JavaScript stops there, and line 3 never runs.

Now step through the experiment below. Pick a value to throw. Then see exactly what the catch gets.

Throw different values and see what the catch gets
Step 0 of 6Ready
Your turn: follow the blue line

You can throw anything. But only an Error object gives the catch the same parts every time.

Running in
  1. script
Next: line 9
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");} try {} 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);}
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Choose what throwValue throws when line 9 calls it

A new choice starts the replay again. First guess what typeof caught and caught instanceof Error will print.

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.

Compare the results:

  • With throw "plain text", the catch gets text.
  • With throw 42, the catch gets a number.
  • Only throw new Error("File is missing") gives the catch a real error object, with a message and a stack.
The stack is useful, but it looks different in each browser

Most browsers give each Error object a stack. The stack is text that lists the lines that led to the error. Chrome and Node.js run JavaScript with an engine called V8. In V8, the first line of the stack shows your class name when you set this.name. The exact format of the stack is not the same everywhere. Error.captureStackTrace only exists in V8, so don't rely on it in code that must run everywhere.

Your own error class: extending Error

INTERACTIVE

A custom error class is a name for one kind of problem, like "a form input is wrong". It doesn't make the program fail harder. It makes the problem easier to recognize. You already know extends and super from the class inheritance lesson. A custom error needs four small steps:

  • Write extends Error, so your class is based on Error.
  • Call super(message) before you use this. This lets Error set up the message and the stack.
  • Set a clear name, like "ValidationError".
  • Add properties that code can check, like field or statusCode.
A ValidationError that knows which field was wrongPop 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);
  • Line 3 calls super(message), so Error stores the message.
  • Line 4 sets the name that logs will show.
  • Line 5 stores which form field was wrong, so a form can highlight it.
  • Line 9 makes the error.
  • Line 10 reads three properties and prints ValidationError email Email is required.
A cart called error holds three cards. ValidationError is its name, set on line 4. Email is required is its message, from super on line 3. email is its field, set on line 5.ValidationErrorEmail is requiredemailerrorname, set on line 4message, from super on line 3field, set on line 5
Line 9 makes one error object. It holds a name, a message, and the extra field.
Without a name, the error just says ErrorPop out in the code editor (opens in a new tab)JavaScript
class PaymentError extends Error {}
const error = new PaymentError("Card declined");
console.log(error.name);
console.log(String(error));

This class works, but it never sets a name. Line 3 prints Error. Line 4 prints Error: Card declined. Giving the class a new name does not change the error's name property. So a log can't tell that this was a payment problem.

Set the name in the constructorPop out in the code editor (opens in a new tab)JavaScript
class PaymentError extends Error {
  constructor(message) {
    super(message);
    this.name = "PaymentError";
  }
}
const error = new PaymentError("Card declined");
console.log(error.name);
console.log(String(error));

Line 3 still calls super(message), which stores the message and the stack. Line 4 sets this.name. Now line 8 prints PaymentError, and line 9 prints PaymentError: Card declined. A payment problem is easy to spot in a log.

Write the name as plain text

You may see this.name = new.target.name. It copies the class name into name. Before a website goes live, a tool called a minifier often shrinks the code. It can rename your classes to short names like a. Then new.target.name gives a. If your logs or your page depend on the name, write it as plain text, as this lesson does.

The experiment below uses two classes:

  • AppError is for any problem in the app. It extends Error.
  • ValidationError is for a bad form input. It extends AppError.

Each class keeps its name in a static field called errorName. A static field belongs to the class itself, not to each object. Line 4 reads it with this.constructor.errorName, so each class gets its own name. Line 5 uses options?.statusCode ?? 500: the status code passed in, or 500 if there is none. The catch checks the more specific class, ValidationError, first.

Check a ValidationError before an AppError
Step 0 of 5Ready
Your turn: follow the blue line

Your own error class turns a plain failure into a specific kind of error that a catch can recognize.

Running in
  1. script
Next: line 22
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";} try {} catch (error) {  if (error instanceof ValidationError) {    console.log("validation", error.field, error.statusCode);  } else if (error instanceof AppError) {    console.log("app", error.statusCode);  }}
CallStoreChangeResultBlue line: runs next. 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 checks whether an object was made by a class, or by a class that extends it. It looks at how the classes are linked. It does not read the text in error.name. So the name is for people reading logs, and the classes are what your code should check.

error.cause: keep the original error

Sometimes the error you want to report is not the first error that happened. Say your app loads a settings file. JSON.parse throws a SyntaxError, because the text is broken. But the useful message for your app is "Config file is not valid JSON".

That new error should do two things:

  • say what your app was doing
  • keep the original SyntaxError, so you can still see the details
Real-life analogycause is the original report, stapled behind the summary

Something went wrong at work. The manager writes a short summary for the team. The first helper's notes are stapled behind it. The summary is easier to read. The notes still matter when someone needs every detail.

In real life: A manager writes a short summary report
In JavaScript: The outer ConfigError and its message
In real life: The first helper's notes are stapled behind it
In JavaScript: The original SyntaxError, kept in error.cause
In real life: Someone reads each report in turn
In JavaScript: A loop that follows .cause from one error to the next

Every Error constructor takes a second argument: an options object. Its cause property keeps the original error: new Error("Couldn't load profile", { cause: err }). Your own error classes can pass the same options object on to super.

Wrapping a low-level error

STEP THROUGH

A low-level error comes from a small built-in tool, like JSON.parse. It knows nothing about your app. To wrap an error means: catch it, then throw a new error with a clearer message for your app. The new error doesn't throw the old one away. It keeps it in cause.

Wrap a JSON error and keep it as the cause
Step 0 of 6Ready
Your turn: follow the blue line

Wrapping keeps both errors: a clear message for your app, and the original error with its details.

Running in
  1. script
Next: line 27
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;} try {} catch (error) {  console.log(causeChain(error).join(" -> "));}
CallStoreChangeResultBlue line: runs next. 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.

Line 12 is the wrap. The outer catch on line 28 gets a ConfigError with a clear message, and error.cause still holds the original SyntaxError.

Two carts. The cart called error holds a ConfigError with a clear message. An arrow labelled cause points from it to a second cart holding the SyntaxError from JSON.parse.ConfigErrorerrornew, clear messagecauseSyntaxErrorcausefrom JSON.parse
The outer catch gets the ConfigError. Its cause property still holds the original SyntaxError from JSON.parse.

The causeChain function on lines 16 to 24 starts at the outer error. It follows .cause from each error to the next. So line 29 prints both errors:

  • what the app was doing: ConfigError: Config file is not valid JSON
  • what exactly broke: the SyntaxError from JSON.parse

AggregateError: one error that holds several

INTERACTIVE

cause makes a chain: this error happened because of that one. AggregateError makes a bundle: several separate errors happened during one job. AggregateError is built into JavaScript. It has:

  • a message that sums up the problem
  • an errors array that holds each separate error
Put several form errors in one AggregateError
Two errors in one AggregateErrorPop 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
  • consolePick which rules fail, then press Run checks.
Try it yourself

Both rules fail at the start. Press Run checks. Then turn the failures off one at a time and run again.

The code shows both rules failing. The buttons run the same checks with your choices and show what comes out.

In the code, lines 1 to 4 make an array of two errors. Line 5 puts them in one AggregateError, called problem. Lines 6 to 8 print its name, its message, and how many errors it holds: AggregateError, User has validation errors, and 2.

A cart called problem is one AggregateError. It holds two error cards: Email is required, which is errors[0], and Must be 13 or older, which is errors[1].Email is requiredMust be 13 or olderproblemerrors[0]errors[1]an AggregateError
problem is one AggregateError. Its errors array keeps both separate errors, so problem.errors.length is 2.

Here you make an AggregateError yourself, from an array of errors and a message. Promises can make one too. A promise is a value that arrives later, or fails. You'll learn promises later in the course.

Promise.any when every promise failsPop out in the code editor (opens in a new tab)JavaScript
Promise.any([
  Promise.reject(new Error("Primary server failed")),
  Promise.reject(new Error("Backup server failed")),
]).catch((error) => {
  console.log(error.name);
  console.log(error.errors.length);
});

Promise.any waits for the first promise that succeeds. Here both promises fail (the word for this is "reject"). So its catch gets an AggregateError, with one error in errors for each failed promise. Line 5 prints AggregateError, and line 6 prints 2.

Designing useful errors

A good error helps two readers at once:

  • your code, which decides what to do next
  • a person, who reads the log

Give each reader its own part. The message is for people. Extra properties are for code. The table below lists the parts of a useful error. Read each row as: this part, its job, an example.

The parts of a useful error
Which partWhat it doesExample
name or classIt says what kind of problem this is.ValidationError
messageIt says which rule or job failed, for people to read.Email is required
causeIt keeps the original, lower-level error.A SyntaxError from JSON.parse
Extra propertiesThey give code facts it can check.field = "email", statusCode = 400
An HTTP error keeps the status codePop out in the code editor (opens in a new tab)JavaScript
class HttpError extends Error {
  constructor(status, message) {
    super(message);
    this.name = "HttpError";
    this.status = status;
  }
}

const response = { ok: false, status: 404 };
try {
  if (!response.ok) throw new HttpError(response.status, "Profile not found");
} catch (error) {
  console.log(error.name, error.status, error.message);
}

On line 9, response is a made-up object. It stands in for a real reply from fetch. Its status is 404, which means "not found". Line 11 throws an HttpError that keeps that number. Line 13 prints HttpError 404 Profile not found.

Real code could check error.status. It could show a "not found" page for 404, but a "try again" message when the server itself fails.

A code string instead of a classPop out in the code editor (opens in a new tab)JavaScript
function loadProfile() {
  throw Object.assign(new Error("Profile not found"), { code: "PROFILE_NOT_FOUND" });
}

try {
  loadProfile();
} catch (error) {
  if (error.code === "PROFILE_NOT_FOUND") console.log("show sign-in");
}

Line 2 throws a normal Error. Object.assign copies an extra code property onto it. Line 8 checks the code and prints show sign-in.

A code string helps when the error comes from somewhere else, like a server reply. A class can't travel inside a reply, but text can. Write down the codes you use, and plan what to do for codes you don't know.

Handle a code you don't knowPop out in the code editor (opens in a new tab)JavaScript
const error = { code: "TIMEOUT" };
if (error.code === "PROFILE_NOT_FOUND") {
  console.log("show sign-in");
} else {
  console.log("retry later");
}

TIMEOUT is not a code this check knows. So the else branch runs and prints retry later.

Code strings are easy to send anywhere. Inside one program, a class check is stronger. A typo in a class name causes an error you will notice. A typo in a code string just fails to match.

Now sort some messages. A good message says what failed, and often how to fix it. A bad message makes 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 you find 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 just as good."

You are allowed to throw anything. But text and numbers have no name, message, or stack. Throw Error objects instead.

"Setting name makes instanceof work."

instanceof checks which class made the object. name is only a label for people who read logs and stacks.

"A wrapper should replace the original error."

A wrapper should give a clear summary and keep the original. Put the original in cause.

"Check the broadest class first."

Check child classes before their parents. A ValidationError is also an AppError and an Error. If you check Error first, that branch catches everything. Then the ValidationError branch never runs.

"AggregateError is only for promises."

Promises can make one. But you can also make one yourself with new AggregateError(errors, message) whenever 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 the instanceof checks

    Guess the three values before you run 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 the loop prints?

        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 two form errors

          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 happens when throw 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 your own error class set its name as plain text?

              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 7ValidationError extends AppError, and AppError extends Error. In what order should a catch check them?

              Choose an answer to see the explanation.

            Key takeaways

            • throw stops the code until a catch takes over.
            • Throw Error objects. Then the catch gets a name, a message, and a stack, and instanceof works.
            • Your own classes, like ValidationError, let a catch tell problems apart.
            • error.cause keeps the original error behind a clearer summary error.
            • AggregateError holds several separate errors in one object.

            Final definition: throw is how JavaScript reports that something went wrong. Custom errors say what kind of problem it was, carry useful facts, and keep the original error.

            Up next: Error handling strategies.

            CompleteFrontend Clear concepts. Working examples.