Exceptions: try, catch and throw
Saying what went wrong with throw, handling what you can with catch, and giving a kind of trouble its own type so the program can tell them apart.
When the program cannot go on
Some situations have no sensible answer: a price that is negative, a name that is empty, a total that is past its limit. An exception is the way a program says so. throw stops the ordinary flow and sends an object upward, carrying a type that names the kind of trouble and a message that says what happened.
If nothing handles it, the program stops and the message is printed. That is not a failure of the design — it is a loud, early stop instead of a quiet wrong answer three steps later, and quiet wrong answers are far more expensive.
Not every bad input deserves an exception. Text that may or may not be a number is the everyday case: int.TryParse(text, out var value) answers true or false and puts the number in value when it succeeds, without throwing. The rule of thumb is that an exception is for a situation the program did not expect, while a false answer is for a possibility it already knew about. Parsing a form field is the second kind.
Catch what you can handle
try marks the work that might go wrong, and catch marks what to do about it. The catch names the type it can handle: catch (OverBudget error) takes that kind of trouble and lets anything else keep travelling. Inside, error.Message is the sentence the thrower wrote.
The program carries on after the catch block, which is the point: one bad row does not have to end the report. finally is the optional third block, and whatever it contains runs whether the work succeeded or failed — the place for tidying up.
Your own kind of trouble
A type of your own is what makes the handling specific. class OverBudget : Exception with a constructor that passes the message to base gives the program a name for one situation, so a catch can handle that and not others. Catching everything with a bare catch is the habit to avoid: it hides the mistakes you have not met yet.
Throw where the problem is found, and at the edge where data arrives: a method that receives a value it cannot use should refuse it there rather than carry it along and fail somewhere unrelated. The exception then means one thing — this input is wrong — and the message can say which one and why.
A type of trouble, a throw and a catch
using System;
// One kind of trouble, with a name of its own.
class OverBudget : Exception
{
public OverBudget(string message) : base(message) { }
}
class Program
{
// Refuse bad input where it arrives.
static decimal Spend(decimal total, decimal limit)
{
if (total > limit)
{
throw new OverBudget("over the limit");
}
return total;
}
static string Label(string name)
{
if (name == "")
{
throw new ArgumentException("the name is empty");
}
return name.Trim();
}
static void Main()
{
try
{
Console.WriteLine(Spend(40m, 50m)); // 40
Label("");
}
catch (ArgumentException error)
{
Console.WriteLine("Bad input: " + error.Message);
}
catch (OverBudget error)
{
Console.WriteLine("Refused: " + error.Message);
}
finally
{
Console.WriteLine("This always runs");
}
}
}A limit that refuses
Run it and read the two lines. Then change the second amount so it passes the limit, and add a third call that goes over by a different amount.
A catch that swallows every exception hides the one you have never seen. Name the types you can actually do something about, and let the rest travel.
The exception travels upward
When a method throws, it stops immediately and the runtime looks for a matching catch in that method. If there is none, the method exits and the search continues in its caller, then in the caller of that method, and so on. Each frame it leaves is discarded, which is why this is called unwinding the stack.
That is why a throw deep inside a helper can be handled at the top of the program, and why a finally in any frame it passes through still runs. The path is no longer a straight line: the normal flow ends at the throw and resumes at the catch.
Throw with a type, catch with a reason
The type is the part a caller can act on; the message is for the person reading the log. A catch that names OverBudget can decide to retry with a bigger limit, while an ArgumentException in the same method means the caller passed nonsense and a retry will not help.
throw; inside a catch rethrows the same exception and keeps its original stack trace. throw error; throws the object again from the current line and loses where it started. Use the first when you meant to let it travel, and the second only when you are wrapping it in a new exception on purpose.
Three exception mistakes: throw error; when you meant throw;, which hides the original location; using exceptions for an ordinary branch, such as a missing optional value that a TryGetValue could report without the cost of a throw; and letting a finally block throw, which replaces the exception already on its way out.
Try these next
- Add a
finallyblock that prints a closing line, then make the try succeed and fail in turn. - Catch a specific exception, then a broader one below it, and check which one handles which throw.
- Write a method that throws on an empty name, call it from another method, and catch it in
Mainto see the exception travel.
A label that refuses an empty name
Run it and read the six lines. Then add a name that is only spaces to the array and check that the guard treats it the same as an empty one.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What does throw do?
