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:
public void Withdraw(int amount){ if (amount > 0 && amount <= Balance) { Balance -= amount; }}The ATM uses it like this:
Account account = new Account(100);account.Withdraw(500);Console.WriteLine("Take your money.");Console.WriteLine("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:
public void Withdraw(int amount){ if (amount <= 0) { throw new ArgumentException("The amount must be positive"); } if (amount > Balance) { throw new ArgumentException("Not enough money"); } Balance -= amount;}throw ends the method like return. The line Balance -= amount is never reached. But instead of a value, the method sends an exception, and the caller cannot miss it:
Account account = new Account(100);try{ account.Withdraw(500); Console.WriteLine("Take your money.");}catch (ArgumentException e){ Console.WriteLine("Declined: " + e.Message);}Console.WriteLine("Balance: €" + account.Balance);Declined: Not enough moneyBalance: €100newcreates the exception object, like any object. The message goes into its constructor.ArgumentExceptionis the ready-made exception for "the argument is wrong". You use it in exactly this kind of check.
More precisely, if you are curious
ArgumentOutOfRangeExceptionis for a number outside the allowed limits, andInvalidOperationExceptionis for an object that is not in a state for this action, such as withdrawing from a closed account.throwworks only with an object of a subclass ofException.
See it
The exception falls through
public void Withdraw(int amount){ if (amount > Balance) { throw new ArgumentException("Not enough money"); } Balance -= amount;}static void PayRent(Account account){ account.Withdraw(800); Console.WriteLine("The rent is paid.");}
try{ PayRent(new Account(500));}catch (ArgumentException e){ Console.WriteLine("Declined: " + e.Message);}Step by step, in words
- Main 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 Main catches the exception, and the program continues from here.
Remember the call stack from the recursion lesson. Main calls PayRent, and it calls Withdraw. The frames sit one on top of another.
When Withdraw throws, the exception looks for a 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 Main has a catch, and the program continues from there.
If Main had no catch either, it would be thrown away too. Then the program stops and prints a stack trace: exactly these frames, from the inside out.
Try it
What reaches the screen?
The account has €50, and the order is €120.
What will the program print?
static void Checkout(Account account){ Console.WriteLine("Paying for the order"); account.Withdraw(120); Console.WriteLine("The order is paid");}
Account account = new Account(50);try{ Checkout(account); Console.WriteLine("Sending confirmation");}catch (ArgumentException e){ Console.WriteLine("Declined: " + e.Message);}Console.WriteLine("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 Main, "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 Main, and that will tell the person.More precisely, if you are curious
- Sometimes a method catches, does something, such as writing to a log, and throws the same exception on:
throw;incatch. Onlythrow;, without a name, keeps the original stack trace.throw e;starts it over from this line. - You can also throw a different exception that says better what happened. Then pass the old one as the second argument of the constructor:
new InvalidOperationException("The rent is not paid", e). You will see both in the stack trace.
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 InsufficientFundsException : Exception{ public int Shortfall { get; }
public InsufficientFundsException(int shortfall) : base("Short by €" + shortfall) { Shortfall = shortfall; }}Your own exception is an ordinary class that inherits from Exception. The constructor passes the message to the parent with : base(...). The method throws it when there is not enough money:
public void Withdraw(int amount){ if (amount <= 0) { throw new ArgumentException("The amount must be positive"); } if (amount > Balance) { throw new InsufficientFundsException(amount - Balance); } Balance -= amount;}And the caller catches the two cases separately:
Account account = new Account(100);try{ account.Withdraw(130);}catch (InsufficientFundsException e){ Console.WriteLine(e.Message); Console.WriteLine("Deposit €" + e.Shortfall + " more or choose a smaller amount.");}catch (ArgumentException e){ Console.WriteLine("Declined: " + e.Message);}Short by €30Deposit €30 more or choose a smaller amount.More precisely, if you are curious
- Exception names always end in
Exception. - Your own exception can also inherit from a more specific ready-made class, such as
ArgumentException. Then acatchfor the parent catches it too. Here it is separate, so the order of the twocatchblocks does not matter.
The idea
Your own or ready-made?
In C# your own exception inherits from Exception, and that is it: there are no checked exceptions. The question is a different one: do you need your own at all?
A ready-made exception, when the meaning matches: ArgumentException for a wrong argument, InvalidOperationException for an action at the wrong moment. Your own, when the caller has to tell it apart from the others and react in its own way, or when the exception has to carry data, like shortfall.
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 InvalidAgeException : Exception
{
public InvalidAgeException(string message) : (message)
{
}
}: base(message) calls the parent's constructor. The parent keeps the message, and 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.
- An abstract class
MusicalInstrumentwith a name, a brand and a price (read-only properties), and a constructor. - An interface
IPlayablewith a methodPlay(). GuitarandPianoinherit from the instrument and can play.Drumonly inherits.NotPlayableException : Exception.MusicShopwith a methodDemoInstrument(MusicalInstrument instrument): if the instrument can play (is IPlayable playable), it plays it, otherwise it throwsNotPlayableException.
- In Main, 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(int amount) that throws ArgumentException for an amount of 0 or less. Withdrawing throws InsufficientFundsException 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 Age property throws InvalidAgeException from the card before this one if the age is below 0 or above 150. The constructor also goes through 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 ownNoTablesException. 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(int qty). If there is not enough in stock, throwOutOfStockException, 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
NotPlayableExceptiona field with the instrument that cannot play, and print its price incatch.
Test