Exceptions, and catching the type you actually expect
try/catch/finally, catching specific types first, and why swallowing an exception is worse than not catching it.
After this lesson you can
- Write a try/catch/finally and say what each part guarantees
- Catch a specific exception type instead of a bare catch
- Explain what a custom exception class is for
try
{
var result = Divide(a, b);
Console.WriteLine(result);
}
catch (DivideByZeroException e)
{
Console.WriteLine($"cannot divide: {e.Message}");
}
finally
{
Console.WriteLine("always runs");
}
finally runs whether try succeeded, threw an exception that was
caught, or threw one that was not — even after a return inside
try. It is where cleanup belongs when the type itself is not
IDisposable and using does not apply.
A catch clause with no type — catch { ... } — catches everything,
including exceptions the code has no idea how to handle correctly. An
empty catch block is worse than no catch at all: the program
continues as if nothing happened, with whatever caused the exception
now silently unaddressed, and no trace of it anywhere.
Try it
public static class Solution{ public static string Describe() { try { int[] nums = { 1, 2, 3 }; return "value=" + nums[5]; } catch (IndexOutOfRangeException e) { return "index: " + e.GetType().Name; } catch (Exception e) { return "generic: " + e.GetType().Name; } }}Catch the most specific type first
try { Process(data); }
catch (FileNotFoundException e) { /* a subclass of IOException */ }
catch (IOException e) { /* the broader type */ }
C# checks catch clauses top to bottom and runs the first one whose
type the thrown exception matches. Putting IOException first would
catch everything below it too, and the compiler refuses to compile an
unreachable catch clause that follows — the ordering is enforced,
not just good style.
Custom exceptions
public class InsufficientFundsException : Exception
{
public decimal Needed { get; }
public decimal Available { get; }
public InsufficientFundsException(decimal needed, decimal available)
: base($"needed {needed}, had {available}")
{
Needed = needed;
Available = available;
}
}
A custom exception is a real class, usually with almost no logic in
it — its job is to give a catch clause something specific to match,
and to carry whatever data the caller needs to react correctly, rather
than parsing that information back out of a generic message string.
Let what you cannot handle travel on
Catching is a promise that this code knows what to do about the failure. When it does not, the right move is to let the exception reach a caller that does, either by not catching it at all or by rethrowing. How you rethrow matters:
using System.Diagnostics;using System.Runtime.CompilerServices; public static class Solution{ [MethodImpl(MethodImplOptions.NoInlining)] static void Inner() => throw new InvalidOperationException("boom"); [MethodImpl(MethodImplOptions.NoInlining)] static void RethrowBare() { try { Inner(); } catch (InvalidOperationException) { throw; } } [MethodImpl(MethodImplOptions.NoInlining)] static void RethrowVariable() { try { Inner(); } catch (InvalidOperationException e) { throw e; } } // The method at the top of the exception's stack trace. static string TopFrame(Action action) { try { action(); return "none"; } catch (Exception e) { return new StackTrace(e).GetFrame(0)!.GetMethod()!.Name; } } public static string Describe() => $"throw; -> {TopFrame(RethrowBare)} | throw e; -> {TopFrame(RethrowVariable)}";}throw; rethrows the exception that is being handled with its stack
trace intact, so the trace still starts at Inner, where the failure
happened. throw e; throws the same object as if it were new, and the
trace now starts at the catch block: the line that actually failed
is gone from the log.
Common mistakes
catch (Exception)around code that can fail in more than one way. A dictionary's indexer throwsKeyNotFoundExceptionfor an absent key butArgumentNullExceptionfor anullkey: the first is ordinary missing data, the second is a bug in whoever built the request. A broad catch files both under the same heading. The exercise below is built to notice.throw e;instead ofthrow;. Shown above: the original stack trace is replaced. To add context, wrap instead:throw new ImportException("row 7", e);keeps the original asInnerException.- An empty
catchblock. The program carries on in a state nobody checked, and nothing is logged. - Exceptions for expected outcomes. If bad input is normal, test for
it (
TryGetValue,int.TryParse) rather than paying for a throw on every bad row.
Exercise · Pro
Report missing keys, and let a null key reach the caller
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade