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.
- 01Throw the right thingExplain why an
Errorobject is easier to catch and handle than text or a plain object. - 02Make your own errorsWrite a class that extends
Error, set itsname, and add properties likefieldorstatusCode. - 03Keep the detailsKeep the original error in
causewhen you wrap it, and put several errors in oneAggregateError.
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, likeValidationError.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.
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
throwstatement 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
ValidationErrororConfigErrorsays 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.
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 errorJSON.parsethrows 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 printsSyntaxError. - 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 THROUGHA 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.
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.
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.
You can throw anything. But only an Error object gives the catch the same parts every time.
script
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);}Compare the results:
- With
throw "plain text", thecatchgets text. - With
throw 42, thecatchgets a number. - Only
throw new Error("File is missing")gives thecatcha real error object, with amessageand astack.
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
INTERACTIVEA 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 onError. - Call
super(message)before you usethis. This letsErrorset up themessageand thestack. - Set a clear
name, like"ValidationError". - Add properties that code can check, like
fieldorstatusCode.
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), soErrorstores the message. - Line 4 sets the
namethat 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.
name, a message, and the extra field.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.
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.
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:
AppErroris for any problem in the app. It extendsError.ValidationErroris for a bad form input. It extendsAppError.
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.
Your own error class turns a plain failure into a specific kind of error that a catch can recognize.
script
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); }}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
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
ConfigErrorand its message - In real life: The first helper's notes are stapled behind it
- In JavaScript: The original
SyntaxError, kept inerror.cause - In real life: Someone reads each report in turn
- In JavaScript: A loop that follows
.causefrom 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 THROUGHA 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.
Wrapping keeps both errors: a clear message for your app, and the original error with its details.
script
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(" -> "));}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.
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
SyntaxErrorfromJSON.parse
AggregateError: one error that holds several
INTERACTIVEcause 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
messagethat sums up the problem - an
errorsarray that holds each separate error
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);- console
Pick which rules fail, then press Run checks.
Both rules fail at the start. Press Run checks. Then turn the failures off one at a time and run again.
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.
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([
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.
| Which part | What it does | Example |
|---|---|---|
name or class | It says what kind of problem this is. | ValidationError |
message | It says which rule or job failed, for people to read. | Email is required |
cause | It keeps the original, lower-level error. | A SyntaxError from JSON.parse |
| Extra properties | They give code facts it can check. | field = "email", statusCode = 400 |
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.
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.
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.
- Email is required
- Failed
- Config file is not valid JSON
- Oops!!!
- Age must be 13 or older
- undefined is not valid
Put each message where it belongs. Imagine you find it in a log at the end of a long day.
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 EXERCISESRun the starter program. What exact line does it print?
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);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);After super(message), the constructor sets name to ValidationError and field to email. The last line joins the name, the field, and the message with colons.
Guess the three values before you run the code.
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);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);ValidationError extends AppError, and AppError extends Error. So all three checks print true.
What two lines print?
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);
}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);
}Line 5 prints the new error's own message. Line 6 reads wrapped.cause.name, the name of the original error: SyntaxError.
What is the first line the loop prints?
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;
}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;
}The loop starts at top, the summary error, and works down to the first error that happened. So the first line is the name and message of top.
What name and length print?
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);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);The object's name is AggregateError. Its errors array keeps both errors, so its length is 2.
Check your understanding
7 QUESTIONSQuestion 1 of 7What happens when
throwruns?Choose an answer to see the explanation.
Question 2 of 7What does the Error instanceof snippet print?
Read the code, then predicttry { throw new Error("No token"); } catch (error) { console.log(error instanceof Error); }Choose an answer to see the explanation.
Question 3 of 7Why should your own error class set its
nameas plain text?Choose an answer to see the explanation.
Question 4 of 7What does the subclass instanceof AppError snippet print?
Read the code, then predictclass 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.
Question 5 of 7What belongs in
error.cause?Choose an answer to see the explanation.
Question 6 of 7What does the AggregateError snippet print?
Read the code, then predictconst 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.
Question 7 of 7
ValidationErrorextendsAppError, andAppErrorextendsError. In what order should a catch check them?Choose an answer to see the explanation.
Key takeaways
throwstops the code until acatchtakes over.- Throw
Errorobjects. Then thecatchgets aname, amessage, and astack, andinstanceofworks. - Your own classes, like
ValidationError, let acatchtell problems apart. error.causekeeps the original error behind a clearer summary error.AggregateErrorholds 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.