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 with intentExplain why Error objects are better caught values than strings or plain objects.
- 02Create custom errorsExtend Error, set a reliable name, and add fields such as field or statusCode.
- 03Preserve 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.
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
throwstatement 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
catchhandles it - In real life: A specific alarm says smoke is in the kitchen
- In JavaScript: A custom class says
ValidationErrororConfigError
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 THROUGHA 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.
Throwing anything is legal, but throwing Error objects gives catch blocks a predictable shape.
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");} 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);}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.
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
INTERACTIVEA 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 usingthis. - Set a readable
name, often with a fixed string or static class field. - Add stable fields, such as
fieldorstatusCode.
The playground uses AppError as the base class and ValidationError as the specific class. The catch block checks the most specific type first.
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.
Custom error classes turn a generic failure into a specific signal a catch block can route.
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";} saveUser({});} catch (error) { if (error instanceof ValidationError) { console.log("validation", error.field, error.statusCode); } else if (error instanceof AppError) { console.log("app", error.statusCode); }}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.
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
ConfigErrormessage - In real life: The first responder's original notes are stapled behind it
- In JavaScript: The original
SyntaxErrorinerror.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 THROUGHWrapping 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.
Wrapping errors preserves both levels: the useful application message and the original technical cause.
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;} parseConfig("{ bad json }");} catch (error) { console.log(causeChain(error).join(" -> "));}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
INTERACTIVEcause 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.
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
Choose failures, then run validation.
Start with two failing rules. Run validation, then turn failures off one at a time and run again.
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.
| Piece | Job | Example |
|---|---|---|
name or class | Names the category of failure | ValidationError |
message | Explains the failed rule or operation | Email is required |
cause | Keeps the lower-level original error | A SyntaxError from JSON.parse |
| Extra fields | Give handlers stable facts | field = "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.
- 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 finding it in a log at the end of a long day.
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 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);The class keeps normal Error behavior, sets a readable name, and adds the field property handlers can use.
Predict the three booleans before running 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);Because ValidationError extends AppError, which extends Error, all three checks are 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);
}The wrapper prints its own message first, then reads the original error name from wrapped.cause.
What is the first line printed by the loop?
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;
}Cause chains read from the summary error toward the root cause, so the first line is the top-level message.
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 aggregate has the built-in name and keeps both individual errors in .errors.
Check your understanding
7 QUESTIONSQuestion 1 of 7What does
throwdo when it runs?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 a custom error class set a fixed
name(or a carefully controlled equivalent)?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 7Which catch order is safest for a hierarchy?
Choose an answer to see the explanation.
Key takeaways
throwstops normal flow until acatchtakes responsibility.- Throw
Errorobjects so handlers getname,message, stack information, andinstanceof. - Custom subclasses such as
ValidationErrormake catch blocks precise. error.causepreserves the lower-level incident behind a higher-level summary.AggregateErrorreports 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.