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}`);Take your money.Balance: €100The 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}`);Declined: Not enough moneyBalance: €100newcreates the exception object, like any object. The message goes into its constructor, ande.messagegives it back.RangeErroris 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 useErroror your own exception. throwin JavaScript accepts any value, even a string:throw "Not enough money"works. But thencatchhas noe.messageand noe.stack(both areundefined), and there is no stack trace. So throw onlyErroror 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
- The program calls payRent for an account with €500.
- payRent calls withdraw(800): a new frame on top.
- 800 > 500: the method throws an exception.
- The balance does not change: the frame is thrown away without finishing its lines.
- payRent has no catch: "The rent is paid." is not printed, and the frame is thrown away.
- 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));/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.2Node 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}`);"Paying for the order" comes before the withdrawal. Then withdraw throws, and the frame of checkout is thrown away: "The order is paid" never shows up. In the program, "Sending confirmation" is skipped too, because it is in try after the call. catch prints the refusal, and the balance is untouched.
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.
catch catches the exception and does nothing. The error disappears, just like with the ATM in the first card. You catch where you can do something sensible. Here payRent cannot find the money, so it should not catch. Remove the try/catch: the exception will reach the program, and that will tell the person.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;incatch. The stack trace stays the original one: it is recorded when theErroris 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}`);}Log: the rent did not go throughDeclined: 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 ine.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}`);}The rent is not paid, because: Not enough moneyThe 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; }}Short by €30Deposit €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. Thene instanceof RangeErroris 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";
}
}super(message) calls the parent's constructor. The parent keeps the message, and e.message gives it back.
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
MusicalInstrumentwith a name, a brand and a price, and a constructor. GuitarandPianoinherit from the instrument and have a methodplay().Drumonly inherits. JavaScript has no interfaces: "can play" means "has aplaymethod".class NotPlayableError extends Error.MusicShopwith a methoddemoInstrument(instrument): if the instrument can play (typeof instrument.play === "function"), it plays it, otherwise it throwsNotPlayableError.
- 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.
Fender Stratocaster, €1299.00The guitar plays a chord.Pearl Export, €849.00Pearl Export cannot be played in a demo.Yamaha P-45, €499.00The 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.
4deposit 50withdraw 500withdraw -5withdraw 120Balance: €150Short by €350. Balance: €150The amount must be positive. Balance: €150Balance: €303. 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).
Maria, 19 years oldPeter: Invalid age: -3Ivan: Invalid age: 200More tasks
- Table reservation. A class
Restaurantwith a number of free tables and a method for reserving one. If the tables have run out, throw your ownNoTablesError. If the number of guests is 0 or less, throw the ready-made exception for a wrong argument. - A product with stock. A class
Productwith a quantity in stock and a methodsell(qty). If there is not enough in stock, throwOutOfStockError, 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
NotPlayableErrora field with the instrument that cannot play, and print its price incatch.
Test