C# Programming · Lesson 9
Some Exceptional Exceptions
Report and handle C# run-time errors with try, catch, finally, throw, custom exceptions, last-chance handlers, and throw expressions.
Lesson purpose
Lesson 9 teaches how C# programs report and recover from run-time errors. Code sometimes fails, and users are unreliable: ask for an int and someone types a double. Good programmers anticipate problems and report them as they occur instead of letting a program silently produce garbage. Students learn the exception mechanism (try, catch, finally, and throw), trace what happens when an exception unwinds the call stack, write a validated factorial program with layered handlers, define a custom exception, plan an error-handling strategy, add a last-chance handler, and use throw expressions.
Learning objectives
- Distinguish run-time errors from compile-time errors.
- Write an exception handler with
try, one or morecatchblocks, andfinally, and state the scope rule for variables declared inside those blocks. - Order
catchblocks from most specific to most general and explain why. - Describe when
finallyruns and the conditions under which it does not. - Trace the sequence of events when an exception is thrown and the call stack unwinds.
- List the benefits of exceptions over older error-reporting techniques.
- Throw standard exceptions with
throw, and know which exceptions should never be caught. - Trace the FactorialException program and its three levels of handling.
- Define a custom exception class derived from
System.Exception. - Apply planning questions and guidelines for robust error handling.
- Find out which methods throw which exceptions, and document your own with XML comments.
- Add a last-chance handler in
Mainand write throw expressions.
1. Run-time errors and the exception mechanism
This lesson is about run-time errors: problems that occur while the program is running, such as bad input, a missing file, or a number too large to compute. Compile-time errors, which C# reports when building the program, are a different category.
The exception mechanism reports run-time errors in a way that the calling method can understand and use to handle the problem. Older techniques relied on return codes that callers could easily ignore; exceptions cannot be ignored silently.
Quick check: is typing “seven” at a numeric prompt a compile-time or run-time error?
2. try, catch, throw, finally
In outline: a method tries to run some code; if the code detects a problem, it throws an error indication; the program catches it; and whatever happens, it finally runs a cleanup block.
public class MyClass
{
public void SomeMethod()
{
try
{
SomeOtherMethod();
}
catch (Exception e)
{
// Runs if an error occurs anywhere in the try block.
// e describes the error in detail.
}
finally
{
// Clean up: close files, release resources.
// Runs whether or not an exception occurred.
}
}
public void SomeOtherMethod()
{
throw new Exception("Description of error");
}
}
- The combination of
try,catch, and (possibly)finallyis an exception handler. - Every method called from inside the
tryblock, and everything they call, is part of thattryblock. - A
tryblock must be followed by at least onecatchblock, afinallyblock, or both.
Scope rule: a variable declared inside a try, catch, or finally block does not exist outside it. Declare it before the block when later code needs it:
int aVariable;
try
{
aVariable = 1;
string aString = aVariable.ToString();
}
catch (Exception e)
{
// exception processing
}
// aVariable is visible here; aString is not.
try blocks
A try block puts the runtime on alert that the code might not work. Keep each try block short and focused on one task or one kind of failure. Several short try blocks are easier to debug and more likely to recover than one giant block around everything.
catch blocks
Control passes to a catch block when an error occurs anywhere in the try block. Three forms:
catch (Exception e) // name the object to use its details
{
Console.WriteLine(e.ToString());
}
catch (FormatException) // type only, when the details are not needed
{
Console.WriteLine("Please enter digits.");
}
catch // bare catch: any exception, like catch (Exception)
{
}
Stack catch blocks from the most specific type to the most general. C# uses the first block whose type matches and skips the rest:
catch (ArgumentOutOfRangeException e) { /* most specific */ }
catch (ArgumentException e) { /* broader */ }
catch (Exception e) { /* last resort */ }
ArgumentOutOfRangeException derives from ArgumentException, which derives from Exception. Placing a general type above a more specific one would make the specific block unreachable, and the compiler reports an error.
finally blocks
A finally block runs:
- whether or not the
tryblock threw an exception, - when
breakorcontinuejumps out of the block, and - when a
returnstatement leaves the method.
It runs after a successful try or after a catch, and a try may have a finally with no catch. Its classic use is cleanup, such as closing a file opened in the try block. Use finally blocks liberally, one per try.
finally does not run when:
- the program terminates because a fatal exception is not handled,
- the
tryblock encounters aStackOverflowException,OutOfMemoryException, orExecutionEngineException, or - an outside application kills the process.
Handlers can be nested: a try…catch inside a try, a catch, or a finally, and a method can contain several handlers.
Quick check: a method returns from inside its try block. Does its finally block run?
3. What happens when an exception is thrown
- An exception is thrown. Deep inside
SomeOtherMethod(), an error is detected and anExceptionobject is thrown. Because this happens at run time, it can occur after the program is released. - C# unwinds the call stack looking for a
catchblock. The exception travels back to the calling method, then that method’s caller, and so on, possibly all the way toMain(). - If an appropriate
catchblock is found, it runs. Appropriate means it names the thrown class or one of its base classes:catch (Exception)handles anything, whilecatch (ArgumentOutOfRangeException)handles only that kind. A method that lacks the context to fix a problem simply has nocatchfor it; the right handler may be higher up. - Any
finallyblocks run before the stack unwinds to the next-higher method, including everyfinallyalong the way. - If no handler exists anywhere, the program crashes. When the exception reaches
Main()uncaught, the user sees an “unhandled exception” message and the program exits.
Benefits of the exception mechanism:
- Expressive: it can model a wide variety of error-handling strategies.
- Informative: an exception object carries its type, message, and stack trace, which aids debugging.
- Readable: the handlers show at a glance what could go wrong, and the code is shorter.
- Consistent: every error is reported the same way, which promotes understanding.
The course treats exceptions as its only error-handling method; low-level operating-system error codes are beyond its scope.
4. Throwing exceptions yourself
Code can throw exceptions just as .NET does:
throw new ArgumentException("Don't argue with me!");
- Prefer a predefined .NET exception when one fits, such as
ArgumentException,ArgumentOutOfRangeException,ArgumentNullException,FormatException,OverflowException,InvalidOperationException, orNotImplementedException. They are documented and familiar to other developers. - Invent a custom exception only when none fits (Section 6).
- Never catch
StackOverflowException,OutOfMemoryException, orExecutionEngineException. They represent ultimate failure: the program has no stack or heap left to keep running, or the exception engine itself is broken.
5. A complete example: FactorialException
A factorial multiplies a whole number by every smaller positive whole number: 4! = 4 × 3 × 2 × 1 = 24, and 0! is defined as 1.
public class MyMathFunctions
{
public static int Factorial(int value)
{
if (value < 0)
{
string s = String.Format("Illegal negative argument to Factorial {0}", value);
throw new ArgumentOutOfRangeException(s);
}
if (value == 0)
{
return 1;
}
int factorial = 1;
do
{
factorial *= value;
if (factorial == 0)
{
string s = String.Format("Input Number {0} Too Large!", value);
throw new OverflowException(s);
}
} while (--value > 1);
return factorial;
}
}
- A negative input throws
ArgumentOutOfRangeException, which is more specific thanArgumentExceptionbecause the problem is a value outside the valid range. - A legal input that is too big to compute throws
OverflowException. --value > 1decrements before comparing, so the loop multiplies down to 2.
Correction for class: by default, int arithmetic that overflows wraps around silently rather than becoming 0. The book’s factorial == 0 test catches very large inputs such as 99 only because the product eventually wraps to exactly 0. An input of 13 produces a wrong answer with no exception. The reliable fix is a checked block, which makes overflow throw OverflowException automatically:
checked
{
factorial *= value; // throws OverflowException on overflow
}
Calling Factorial with layered handlers
string input = string.Empty;
while (input.ToLower() != "quit")
{
Console.WriteLine("Enter a positive number or Quit: ");
input = Console.ReadLine() ?? "";
if (input.ToLower() == "quit")
{
continue;
}
if (!int.TryParse(input, out int value))
{
Console.WriteLine("Please enter an integer number or Quit!");
continue;
}
int result = 0;
try
{
result = MyMathFunctions.Factorial(value);
}
catch (ArgumentOutOfRangeException)
{
Console.WriteLine("You must enter a positive number!");
continue;
}
catch (OverflowException)
{
Console.WriteLine("The number supplied is too large!");
continue;
}
catch (Exception e)
{
Console.WriteLine("An unexplainable error has happened!");
Console.WriteLine(e.ToString());
Environment.Exit(-1);
}
finally
{
Console.WriteLine("Thank you for testing the application!");
}
Console.WriteLine($"The factorial of {value} is {result}.");
}
Talking points:
- Validate first:
TryParserejects “S” and “2.2” beforeFactorialis ever called, so exceptions are reserved for truly exceptional cases. - Short
tryblock: it surrounds only the call that can fail. The result prints after the handler succeeds. - Three levels of handling: bad input (give the user another chance), a valid number too large to compute (explain and retry), and anything unexpected (show details and exit gracefully with
Environment.Exit(-1), a nonzero error code). finallystill runs even though eachcatchusescontinue, which is why “Thank you for testing” appears after the error messages.- The book lists the more likely error first; the essential rule is that more specific types must come before
Exception.
Sample run: “S” and “2.2” ask for an integer; -5 prints the positive-number message and the thank-you line; 99 prints the too-large message and the thank-you line; 5 prints the thank-you line and then “The factorial of 5 is 120.”
Prediction: what does the program print for 0? For 13 with and without checked?
6. Custom exceptions
A custom exception is a class derived from System.Exception:
public class CustomException : Exception
{
public CustomException() : base()
{
}
public CustomException(string message) : base(message)
{
}
public CustomException(string message, Exception innerException)
: base(message, innerException)
{
}
}
- The
: Exceptionin the heading is inheritance (Part 2): the new class gets all ofException’s behavior. - Each constructor passes its arguments to the base class with
: base(...), so no extra code is needed. - The inner exception constructor lets code catch a low-level exception and wrap it in a more meaningful one while preserving the original.
- The book also shows a fourth,
protectedconstructor that takesSerializationInfoandStreamingContext. That serialization pattern is obsolete in current .NET and can be omitted.
Custom exceptions are worth writing when they add something: more debugging information, clearer messages, or a domain-specific type, such as turning a DivideByZeroException into an InvalidSalesTaxRateException for the caller.
7. Planning an exception-handling strategy
Questions to ask
- What could go wrong? Ask it about every piece of code.
- If it goes wrong, can I fix it? If so, the program may be able to recover and continue.
- Does the problem put user data at risk? Protect data from loss or damage at all costs.
- Where should the handler go? The method where the exception occurs often lacks context. A method higher in the call chain may know better what to do, so place the
tryaround the call there. - Which exceptions should I handle? All of them, at least by exiting gracefully; never leave the user with a frightening crash dialog.
- What about exceptions that slip through the cracks? Test, test, and test again.
- What input errors might occur? Not just from users: files can hold bad data or be missing or locked, and networks fail. Check range, type, and availability.
- How robust (unbreakable) must the code be? A small utility may end up somewhere critical, so write every program as if it matters.
Guidelines for code that handles errors well
- Protect the user’s data at all costs.
- Don’t crash. Recover if possible; otherwise shut down gracefully with a clear, helpful message.
- Don’t keep running after an unrecoverable problem; the program or its data could be left inconsistent.
Environment.FailFast()terminates immediately and deliberately. - Treat class libraries differently from applications: libraries let exceptions reach the caller; applications handle what they can.
- Throw an exception whenever a method cannot complete its task. Fix invalid input and continue when possible; otherwise throw.
- Prefer code that doesn’t need exceptions, and fix bugs rather than patching them with handlers, but use exceptions as the main way to report errors.
- Don’t catch an exception unless the method can do something useful with it. Most methods have no handlers.
- Test every category of bad input: negative numbers, zero, very large values, empty strings,
null, and failing resources such as files, databases, and URLs. - Catch the most specific exception possible; avoid many handlers for broad types such as
Exception. - Always provide a last-chance handler at the top of the program (not in reusable libraries).
- Don’t use exceptions for normal flow, such as leaving a loop.
- Write custom exceptions when they add information.
Exceptions that a public method can throw are part of its public interface and must be documented.
8. Finding out which methods throw which exceptions
- Tooltips: hovering over a method name in Visual Studio shows its parameters, return type, and the exceptions it can throw. Right-click and choose Peek Definition for more.
- Documentation: click a .NET method and press F1 to open its documentation, which lists its exceptions with descriptions.
- XML comments for your own methods: Visual Studio shows them in IntelliSense and the Object Browser.
/// <summary>
/// Return the factorial of the provided value.
/// </summary>
/// <param name="value">The number whose factorial is computed.</param>
/// <returns>The factorial as an int.</returns>
/// <exception cref="ArgumentOutOfRangeException">
/// The value must not be negative.
/// </exception>
/// <exception cref="OverflowException">
/// The factorial is too large to compute.
/// </exception>
public static int Factorial(int value)
For each listed exception, decide how likely it is and guard against it when the program warrants.
9. The last-chance handler
Any exception not caught elsewhere percolates up to Main(), the program’s starting and ending point. Wrapping the contents of Main() in a try block with a catch (Exception) is the last opportunity to intercept the error before the operating system shows a much harder-to-read message.
static void Main(string[] args)
{
try
{
RunProgram();
}
catch (Exception e)
{
Console.WriteLine("Sorry, the program has to close because of an unexpected problem.");
LogError(e); // save the details for later analysis
}
}
- During development, show the full technical details so problems are visible.
- In the released version, translate the details into plain English, suggest what the user might do next time, log the technical information, and exit.
- Demonstrate the need: throw an exception deliberately with no handler and show students what users would otherwise see.
Optional modern note: AppDomain.CurrentDomain.UnhandledException offers another last-chance hook for logging exceptions that escape every handler.
10. Throw expressions
Before C# 7.0, throw was only a statement. Code had to finish an expression and then test the result:
var myStrings = "One,Two,Three".Split(',');
var numbers = (myStrings.Length > 0) ? myStrings : null;
if (numbers == null)
{
throw new Exception("There are no numbers!");
}
C# 7.0 and later allow a throw expression wherever an expression is expected, including after the null-coalescing operator ?? and in the conditional operator:
var numbers = myStrings ?? throw new Exception("There are no numbers!");
var numbers2 = (myStrings.Length > 0)
? myStrings
: throw new Exception("There are no numbers!");
?? returns its left operand unless it is null; here, a null triggers the exception.
Throw expressions also work in expression-bodied members (Part 2). A method with no content yet can say so clearly instead of returning a misleading null:
public string GetMyString() => throw new NotImplementedException();
Returning null would leave the caller unsure whether the method failed, and a full method body just to throw is cumbersome. (The book’s => return null; form is not valid syntax; an expression-bodied member contains only the expression.)
Classroom application
- Run a program that divides by user input with no handler, enter 0, and read the unhandled-exception message together.
- Add
TryParsevalidation, then atryblock around the division withcatch (DivideByZeroException)and a finalcatch (Exception). Add afinallythat prints “Done.” - Reorder the
catchblocks soExceptioncomes first and read the compiler error. - Build the factorial program, test 5, 0, -5, 13, and 99, then add
checkedand retest 13. - Write
InvalidScoreException : Exceptionand throw it when a score is outside 0–100. - Wrap
Mainin a last-chance handler and add XML<exception>comments toFactorial.
Common misconceptions
- Exceptions are run-time errors; the compiler does not catch them.
- A
tryblock needs acatch, afinally, or both. - Variables declared inside a
tryblock are not visible after it. catchorder matters: the first matching block wins, so specific types go first.finallyruns even afterreturn,break, orcontinue, but not when the process itself dies.- Catching an exception without being able to handle it helps no one.
- Exceptions should not replace ordinary control flow or input validation.
- Integer overflow does not throw by default; use
checkedwhen overflow must be detected. StackOverflowException,OutOfMemoryException, andExecutionEngineExceptionshould never be caught.??existed before C# 7.0; what C# 7.0 added is the ability tothrowinside an expression.
Lesson summary
The exception mechanism reports run-time errors so that the caller best positioned to respond can handle them. A try block marks risky code, catch blocks handle specific failures from most to least specific, finally cleans up, and throw raises standard or custom exceptions. When an exception is thrown, C# unwinds the call stack to the nearest matching handler, running finally blocks along the way, and crashes only if no handler exists. Robust programs validate input first, protect user data, catch only what they can handle, document what they throw, and end with a last-chance handler in Main. Lesson 10 revisits enumerations in depth.
Course Notes