Skip to content
wannacode

Throwing and your own exceptions

Problem

The ATM says "take your money"

The account keeps its balance. If the amount to withdraw is negative or there is not enough money, the method simply does nothing:

withdraw(amount) {
if (amount > 0 && amount <= this.balance) {
this.balance -= amount;
}
}

The ATM uses it like this:

const account = new Account(100);
account.withdraw(500);
console.log("Take your money.");
console.log(`Balance: €${account.balance}`);
Output
Take your money.
Balance: €100

The customer asks for €500 and has €100. The method quietly refuses, the ATM does not notice and says "Take your money." The error gets lost. The method could return true or false, but the caller can easily forget to check, and false does not say why.

The idea

throw: I refuse and I say why

Instead of keeping quiet, the method throws an exception:

withdraw(amount) {
if (amount <= 0) {
throw new RangeError("The amount must be positive");
}
if (amount > this.balance) {
throw new RangeError("Not enough money");
}
this.balance -= amount;
}

throw ends the method like return. The line this.balance -= amount is never reached. But instead of a value, the method sends an exception, and the caller cannot miss it:

const account = new Account(100);
try {
account.withdraw(500);
console.log("Take your money.");
} catch (e) {
console.log(`Declined: ${e.message}`);
}
console.log(`Balance: €${account.balance}`);
Output
Declined: Not enough money
Balance: €100
  • new creates the exception object, like any object. The message goes into its constructor, and e.message gives it back.
  • RangeError is the ready-made exception for "the value is out of range". The amount here is just that: below zero or above the balance.
  • catch (e) in JavaScript has no kind in the parentheses and catches every exception. Here there is only one exception, so that is enough.
More precisely, if you are curious
  • Another common ready-made exception is TypeError: the value is of the wrong type. For everything else you use Error or your own exception.
  • throw in JavaScript accepts any value, even a string: throw "Not enough money" works. But then catch has no e.message and no e.stack (both are undefined), and there is no stack trace. So throw only Error or a subclass of it.

See it

The exception falls through

withdraw(amount) {
if (amount > this.balance) {
throw new RangeError("Not enough money");
}
this.balance -= amount;
}
function payRent(account) {
account.withdraw(800);
console.log("The rent is paid.");
}
try {
payRent(new Account(500));
} catch (e) {
console.log(`Declined: ${e.message}`);
}
Step by step, in words
  1. The program calls payRent for an account with €500.
  2. payRent calls withdraw(800): a new frame on top.
  3. 800 > 500: the method throws an exception.
  4. The balance does not change: the frame is thrown away without finishing its lines.
  5. payRent has no catch: "The rent is paid." is not printed, and the frame is thrown away.
  6. The catch in the program catches the exception, and the program continues from here.

Remember the call stack from the recursion lesson. At the bottom is the program: the top level code. It calls payRent, and that calls withdraw. The frames sit one on top of another.

When withdraw throws, the exception looks for an catch by going down the stack. A frame without a matching catch is thrown away right away, without finishing its lines. That is what happens to payRent too. Only the program has an catch, and it continues from there.

If there were no catch there either, the program would stop and print a stack trace: exactly these frames.

More precisely, if you are curious

Here is the same program, but nobody catches (the file rent.js):

class Account {
constructor(balance) {
this.balance = balance;
}
withdraw(amount) {
if (amount > this.balance) {
throw new RangeError("Not enough money");
}
this.balance -= amount;
}
}
function payRent(account) {
account.withdraw(800);
console.log("The rent is paid.");
}
payRent(new Account(500));
Error
/projects/rent/rent.js:8
throw new RangeError("Not enough money");
^
RangeError: Not enough money
at Account.withdraw (/projects/rent/rent.js:8:13)
at payRent (/projects/rent/rent.js:15:11)
at Object.<anonymous> (/projects/rent/rent.js:19:1)
at Module._compile (node:internal/modules/cjs/loader:1705:14)
at Object..js (node:internal/modules/cjs/loader:1838:10)
at Module.load (node:internal/modules/cjs/loader:1441:32)
at Function._load (node:internal/modules/cjs/loader:1263:12)
at TracingChannel.traceSync (node:diagnostics_channel:328:14)
at wrapModuleLoad (node:internal/modules/cjs/loader:237:24)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:171:5)
Node.js v22.22.2

Node shows the line where the exception was thrown, then the kind and the message, and then the frames from the inside out, like Java: Account.withdraw, payRent, and Object.<anonymous> is the program. The lines with node:internal are the code of Node itself.

Try it

What reaches the screen?

The account has €50, and the order is €120.

What will the program print?

function checkout(account) {
console.log("Paying for the order");
account.withdraw(120);
console.log("The order is paid");
}
const account = new Account(50);
try {
checkout(account);
console.log("Sending confirmation");
} catch (e) {
console.log(`Declined: ${e.message}`);
}
console.log(`Balance: €${account.balance}`);
Answer

Try it

Find the bug

The rent is €800, and the account has €500. The program says "The rent is paid." Which line is the bug on?

Pick the line with the bug.

More precisely, if you are curious
  • Sometimes a function catches, does something, such as writing to a log, and throws the same exception on. In JavaScript that is throw e; in catch. The stack trace stays the original one: it is recorded when the Error is created, not when it is thrown.
function payRent(account) {
try {
account.withdraw(800);
} catch (e) {
console.log("Log: the rent did not go through");
throw e;
}
}
try {
payRent(new Account(500));
} catch (e) {
console.log(`Declined: ${e.message}`);
}
Output
Log: the rent did not go through
Declined: Not enough money
  • You can also throw a different exception that says better what happened, and keep the old one as the cause. In JavaScript: new Error("The rent is not paid", { cause: e }). The old one is in e.cause, and Node prints it under the new one as [cause].
function payRent(account) {
try {
account.withdraw(800);
} catch (e) {
throw new Error("The rent is not paid", { cause: e });
}
}
try {
payRent(new Account(500));
} catch (e) {
console.log(`${e.message}, because: ${e.cause.message}`);
}
Output
The rent is not paid, because: Not enough money

The idea

Your own exception

"Not enough money" is text. If the ATM wants to suggest "Deposit €30 more", it has to know how much is missing. Pulling the number out of the text is fragile. Better make your own exception class with a field:

class InsufficientFundsError extends Error {
constructor(shortfall) {
super(`Short by €${shortfall}`);
this.name = "InsufficientFundsError";
this.shortfall = shortfall;
}
}

Your own exception is an ordinary class that inherits from Error. The constructor passes the message to the parent with super(...). this.name gives the exception its name: without it, e.name and the stack trace say only Error. The method throws it when there is not enough money:

withdraw(amount) {
if (amount <= 0) {
throw new RangeError("The amount must be positive");
}
if (amount > this.balance) {
throw new InsufficientFundsError(amount - this.balance);
}
this.balance -= amount;
}

And the caller catches the two cases separately:

const account = new Account(100);
try {
account.withdraw(130);
} catch (e) {
if (e instanceof InsufficientFundsError) {
console.log(e.message);
console.log(`Deposit €${e.shortfall} more or choose a smaller amount.`);
} else if (e instanceof RangeError) {
console.log(`Declined: ${e.message}`);
} else {
throw e;
}
}
Output
Short by €30
Deposit €30 more or choose a smaller amount.

catch in JavaScript is one, and it catches everything. You check the kind inside with instanceof, and an exception you do not expect, you throw on with throw e;. Otherwise the empty else would swallow it, as in the task a moment ago.

More precisely, if you are curious
  • Exception names always end in Error.
  • Your own exception can also inherit from a more specific ready-made class, such as RangeError. Then e instanceof RangeError is true for it too. Here it is separate, so the order of the two checks does not matter.

The idea

Your own or ready-made?

There are no checked exceptions like in Java here. Nobody checks before the program runs whether the caller catches. What a function throws is written in its documentation: a JSDoc comment with @throws.

/**
* Withdraws the amount from the account.
* @throws {RangeError} if the amount is not positive
* @throws {InsufficientFundsError} if there is not enough money
*/
withdraw(amount) {
if (amount <= 0) {
throw new RangeError("The amount must be positive");
}
if (amount > this.balance) {
throw new InsufficientFundsError(amount - this.balance);
}
this.balance -= amount;
}

This is only a habit, for people and for the editor. A program that calls withdraw without try starts and runs until the exception is really thrown. The question is a different one: do you need your own exception at all?

A ready-made exception, when the meaning matches: RangeError for a wrong value, TypeError for a wrong type. Your own, when the caller has to tell it apart from the others and react in its own way, when the exception has to carry data, like shortfall, or when there is no ready-made one with this meaning, as for withdrawing from a closed account.

Try it

Fill in the blank

An exception for an invalid age. What goes in the blank so that the message reaches the parent?

class InvalidAgeError extends Error {
  constructor(message) {
    (message);
    this.name = "InvalidAgeError";
  }
}

Exercise

Exceptions in classes

Solve these in your editor.

1. Music shop. The shop plays instruments in a demo, but not every one can be played.

  • A class MusicalInstrument with a name, a brand and a price, and a constructor.
  • Guitar and Piano inherit from the instrument and have a method play(). Drum only inherits. JavaScript has no interfaces: "can play" means "has a play method".
  • class NotPlayableError extends Error.
  • MusicShop with a method demoInstrument(instrument): if the instrument can play (typeof instrument.play === "function"), it plays it, otherwise it throws NotPlayableError.
  • In the program, demo a Fender Stratocaster guitar (€1299), Pearl Export drums (€849) and a Yamaha P-45 piano (€499) from an array. Catch the exception for each instrument separately, so the demo goes on.
Output
Fender Stratocaster, €1299.00
The guitar plays a chord.
Pearl Export, €849.00
Pearl Export cannot be played in a demo.
Yamaha P-45, €499.00
The piano plays a scale.

2. An account with rules. Add to Account a method deposit(amount) that throws RangeError for an amount of 0 or less. Withdrawing throws InsufficientFundsError from the lesson. The account starts with €100. Read the number of operations and then the operations themselves, each on its own line: "deposit 50" or "withdraw 500". After each one, print the balance or why the operation was declined.

Input
4
deposit 50
withdraw 500
withdraw -5
withdraw 120
Output
Balance: €150
Short by €350. Balance: €150
The amount must be positive. Balance: €150
Balance: €30

3. Invalid age. Person has a name and an age. The setter set age(value) throws InvalidAgeError from the card before this one if the age is below 0 or above 150. The constructor also goes through this.age = age, so that nobody can create a person with an invalid age. Create Maria (19), Peter (−3) and Ivan (200).

Output
Maria, 19 years old
Peter: Invalid age: -3
Ivan: Invalid age: 200

More tasks

  • Table reservation. A class Restaurant with a number of free tables and a method for reserving one. If the tables have run out, throw your own NoTablesError. If the number of guests is 0 or less, throw the ready-made exception for a wrong argument.
  • A product with stock. A class Product with a quantity in stock and a method sell(qty). If there is not enough in stock, throw OutOfStockError, which carries how many items there are. The shop catches the exception and offers to sell the ones in stock.
  • Music shop with a reason. Add to NotPlayableError a field with the instrument that cannot play, and print its price in catch.

Test

Check yourself

1. What does throw new RangeError("...") do?
2. The program calls a, a calls b, b throws an exception. Only the top level code has a catch. What happens to the lines in a after the call to b?
3. Where is the best place to catch the exception?
4. Why does the constructor of InsufficientFundsError set this.name = "InsufficientFundsError"?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·