Learn by reading through in order

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
}
Code Stops Only When You Pass It to throw
Only new Errorinside the ifAn Error objectis createdNothing happens;next line runs1200 * 0returns 0throw new Errorinside the ifAn Error objectis createdAn exception isthrown right thereThe value reachescatch's error
Both versions create an Error object. Only when it's passed to throw does the code stop right there.

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.

Check the satisfaction rating before saving a survey. scores is already declared.

① If the rating isn't an integer, throw an exception with the message "Rating must be an integer".

② If it's outside 1–5, throw an exception with the message "Rating must be between 1 and 5".

③ If there's no problem, return it in the form "Submitted with a rating of 4".

④ Pass each value in scores to it, keep going even if one fails, and print either the result or the error message.

(If it runs correctly, an explanation will appear.)

JavaScript / TypeScript Editor

Run code to see output

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
}
Where Types Created with extends Error Fit
Error — exception types that have message and name
  • For an exception created with new Error("message") and for every type in this box, instanceof Error is true
Built-in types — provided by JavaScript from the start
  • SyntaxError / TypeError / RangeError — the types from the previous article
Types created with extends Error
  • ValidationError — represents an input error
  • name is the "ValidationError" written inside the class
  • instanceof ValidationError is true only inside this box
Both sit inside the Error box, and neither sits inside the other. instanceof ValidationError is true only inside its own 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 throwname readable in catchResult of instanceof Error
"Invalid ZIP code" (a string)undefined (a string has none)false
new Error("Invalid ZIP code")Errortrue
ValidationError with name setValidationErrortrue
ValidationError without name setError (inherited as-is)true

Show why a file is rejected. QuotaError, files, and checkFile are already declared.

① Define a FormatError that inherits from Error, and set its name.

② In checkFile, throw a FormatError with the message "Only PDFs are accepted" if the extension isn't pdf.

③ Check each file in files and print either the return value or "name: message".

JavaScript / TypeScript Editor

Run code to see output

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");
}
Check Order Decides the Branch
instanceof Errorchecked firsttrue even for aValidationErrorLater checksaren't triedPrints"Couldn't save"ValidationErrorchecked firsttrue: created fromValidationErrorThe Error checkisn't triedPrints "Pleasecheck your input"
In the top row, the first check is true, so the second is never tried. Check types that inherit from Error before Error itself.

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.

Process email address change requests. ValidationError, changeEmail, and requests are already declared.

① Pass each item in requests to changeEmail and print the result.

② For a ValidationError, print "Check your email address: " followed by its message.

③ For any other Error, print "Couldn't change it: " followed by the type's name.

JavaScript / TypeScript Editor

Run code to see output

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
}
The Original Exception Stays Inside cause
error — the PaymentError thrown by readPayment
  • error.name is PaymentError
  • error.message is Can't read the payment result
error.cause — the original exception thrown by JSON.parse
  • error.cause.name is SyntaxError
  • error.cause.message is the description from JSON.parse, unchanged
The outer exception is a PaymentError, and cause holds the original SyntaxError. The original exception is kept intact in error.cause.

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.

Import attendance records. ImportError and records are already declared.

① In importRecords, take the first five characters of each clock-in time and print it in the form "E-104: 09:02".

② If that fails, throw an ImportError with the message "Import stopped at the row for E-105", with the original exception attached.

③ Call importRecords, and if it fails, print the message and then "Original exception: " followed by the original exception's type name.

JavaScript / TypeScript Editor

Run code to see output
QUIZ

Knowledge Check

Answer each question one by one.

Q1What's the name of an exception created from class ShippingError extends Error {}?

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?