Q1What's the name of an exception created from class ShippingError extends Error {}?
Throwing Exceptions — throw and Custom Errors
Throw exceptions with throw new Error, create your own error types with extends Error, and keep the original error with cause.
Even if the order page sends a quantity of 0, the function that works out the subtotal just goes ahead and calculates it, and a 0-yen order goes through. Showing a warning won't stop the caller, either: it doesn't notice the problem and goes on to save the order.
This article covers throw new Error, which lets you throw an exception yourself, and custom errors, which are error types you define.
Stopping on a Bad Value — throw new Error
Say you want to stop before calculating the subtotal when the quantity is a decimal or a negative number. Signaling that with return null isn't enough: if the caller forgets to check, adding shipping with null + 500 gives 500, and the calculation carries on.
An Error object (a value with name and message, made to be thrown as an exception) is created with new Error("message"). Pass it to throw and an exception is thrown right there; if the call was made inside try, that value arrives as the error in catch.
function calcSubtotal(unitPrice, quantity) {
// If it isn't an integer of 1 or more, throw an exception right here
if (!Number.isInteger(quantity) || quantity < 1) {
// new Error alone doesn't stop anything. Pass it to throw to make it an exception
throw new Error(`Quantity must be an integer of 1 or more: ${quantity}`);
}
return unitPrice * quantity;
}
try {
console.log(calcSubtotal(1200, 3)); // 3600
console.log(calcSubtotal(1200, 0)); // throws, so nothing is printed
console.log("Order confirmed"); // this line doesn't run either
} catch (error) {
console.log(error.name); // Error
console.log(error.message); // Quantity must be an integer of 1 or more: 0
}
Put the validation if at the top of the function, before the calculation. throw exits the function on the spot, just like return, so the return unitPrice * quantity; after it doesn't run, and the caller never gets a 0 back.
Creating a Type for Input Errors — extends Error
An exception thrown with new Error always has the name Error, no matter what the validation was about. A ZIP code with the wrong number of digits and an out-of-stock item arrive under the same name, so the catch block can't tell whether it's a failure the user can fix by correcting their input.
Adding extends Error to a class (syntax that defines a type of object you create with new) creates a type that inherits Error's behavior. Write name = "ValidationError"; inside the braces, and exceptions created from this type get that string as their name.
// Inherit from Error to create a type for input errors
class ValidationError extends Error {
name = "ValidationError"; // becomes the name of exceptions created with new
}
function checkZipCode(zipCode) {
if (zipCode.length !== 5) {
throw new ValidationError(`Enter a 5-digit ZIP code: ${zipCode}`);
}
return zipCode;
}
try {
checkZipCode("9021");
} catch (error) {
console.log(error.name); // ValidationError
console.log(error.message); // Enter a 5-digit ZIP code: 9021
console.log(error instanceof Error); // true
}
- For an exception created with
new Error("message")and for every type in this box,instanceof Erroristrue
SyntaxError/TypeError/RangeError— the types from the previous article
ValidationError— represents an input errornameis the"ValidationError"written inside the class- instanceof ValidationError is true only inside this box
You can also pass a string to throw, but a string has neither name nor message, so reading error.message in catch gives undefined. The table below shows what you can read in catch for each kind of value passed to throw.
| Value passed to throw | name readable in catch | Result of instanceof Error |
|---|---|---|
| "Invalid ZIP code" (a string) | undefined (a string has none) | false |
| new Error("Invalid ZIP code") | Error | true |
| ValidationError with name set | ValidationError | true |
| ValidationError without name set | Error (inherited as-is) | true |
Checking Your Own Types First — the Order of instanceof
A user can fix an input error, but an unexpected exception like a TypeError won't go away no matter how they change their input. You want catch to show two different messages, but if you write the checks in the wrong order, even input errors get the message meant for unexpected exceptions.
if and else if are checked from top to bottom, and only the first branch that matches runs. A ValidationError is also true for instanceof Error, so if instanceof Error comes first, a ValidationError ends up in that branch too.
class ValidationError extends Error {
name = "ValidationError";
}
const error = new ValidationError("Enter a 5-digit ZIP code");
// Check Error first, and a ValidationError lands in this branch too
if (error instanceof Error) {
console.log("Couldn't save"); // Couldn't save
} else if (error instanceof ValidationError) {
console.log("Please check your input"); // doesn't get here
}
// Check the subclass first
if (error instanceof ValidationError) {
console.log("Please check your input"); // Please check your input
} else if (error instanceof Error) {
console.log("Couldn't save");
}
Built-in types like TypeError also inherit from Error, so the instanceof Error branch placed last catches all of them. Use that branch to show a different message for exceptions that correcting the input won't fix.
Throwing with the Original Exception Attached — cause and Uncaught Exceptions
Say a SyntaxError is thrown in a function that reads a payment service's response. If you just rethrow it, its type doesn't tell the caller that the failure happened while handling a payment. But if you swap it for an exception of a new type, you lose the fact that it started out as a SyntaxError.
cause (an option that attaches the original exception to a new one) goes in the second argument, as in new Error("message", { cause: error }). It works the same with types created with extends Error, and whoever catches the new exception can read the original from error.cause.
class PaymentError extends Error {
name = "PaymentError";
}
function readPayment(text) {
try {
return JSON.parse(text);
} catch (error) {
// Attach the original exception as cause and throw it as a payment error
throw new PaymentError("Can't read the payment result", { cause: error });
}
}
try {
readPayment('{"orderId":"P-3107","amount":}');
} catch (error) {
console.log(error.name); // PaymentError
console.log(error.message); // Can't read the payment result
console.log(error.cause.name); // SyntaxError
}
error.nameisPaymentErrorerror.messageisCan't read the payment result
error.cause.nameisSyntaxErrorerror.cause.messageis the description from JSON.parse, unchanged
If you swap in a new exception without cause, error.cause is undefined, and you can't read the original type or description. With cause, the caller can use error.name to show a payment-failure message, and still log what's in error.cause.
An Exception Nobody Catches Stops Execution
If no catch receives a thrown exception, it becomes an uncaught exception (an exception that no catch handles), and execution stops right there; none of the remaining lines run. If you remove the outer try from the code above, an error with the type's name and message is shown, such as PaymentError: Can't read the payment result.
Knowledge Check
Answer each question one by one.
Q2If you check instanceof Error first, which branch does a ValidationError exception go into?
Q3You caught an exception thrown with cause attached using catch (error). How do you read the original exception's name?