Error Handling
How C# represents things that go wrong — and how to respond without crashing the program
So far our programs have assumed everything works: files exist, inputs parse, math never overflows, networks never go down. Real programs aren't that lucky.
C# splits "things that go wrong" into two camps:
- Expected failures that you can predict and check for — these
should be handled with regular control flow (return values,
TryParse,boolflags). - Exceptional failures that you can't easily prevent and shouldn't have to write a defensive check for at every call site — these use the exception system.
The discipline of knowing which is which is most of what "good error handling" means.
Exceptions, the mechanism
An exception is an object that represents a failure. When code
throws one, normal execution stops, and the runtime unwinds the
call stack looking for a matching catch block. If no catch is
found, the program crashes.
Without the try/catch, the program would crash with a stack
trace. The catch block lets us respond instead.
The flow of an exception
The exception travels up the stack until something catches it.
Anything in its path (open files, half-built objects) gets cleaned
up by stack unwinding plus finally blocks.
try / catch / finally
Rules:
- The most specific
catchblocks should come first. The runtime picks the first matching one. - A
finallyblock always runs, whether or not an exception was thrown — perfect for cleanup.
Throwing your own exceptions
When your method receives nonsense input or hits an impossible state, the right thing to do is usually to throw, not to silently return wrong data.
A few common built-in exception types:
| Exception | Use when… |
|---|---|
ArgumentException | An argument is invalid for some reason |
ArgumentNullException | A required argument was null |
ArgumentOutOfRangeException | A number/index is outside an allowed range |
InvalidOperationException | The object is in a state where this call isn't allowed |
NotImplementedException | "I plan to write this method, but haven't yet" |
FormatException | A string couldn't be parsed |
KeyNotFoundException | A dictionary key didn't exist |
Exceptions are NOT for ordinary control flow
This is a really common beginner trap:
It works, but it's wasteful and obscures intent. The whole point
of TryParse is to not throw for the perfectly ordinary case
"this string isn't a number":
Rule of thumb: if the failure is normal and frequent, use a return value. Use exceptions only for situations the caller shouldn't have to expect at every call site.
Catching the right thing
Avoid catching Exception (or worse, swallowing all exceptions
silently) unless you're at the very top of the program and just
want to log and exit gracefully. In ordinary code, catch only the
specific exceptions you actually know how to handle.
A silent catch block is one of the most dangerous things in any
codebase. It turns bugs into mysterious wrong answers.
Re-throwing
If you catch an exception only to log or annotate it but don't actually know how to recover, re-throw:
throw; re-throws the same exception. throw ex; looks similar
but loses the original stack trace, making debugging harder.
Null and NullReferenceException
The single most common runtime error in C# is calling a method on
a variable that turned out to be null:
Modern C# (with nullable reference types) lets the compiler warn
you when a variable could be null — making
NullReferenceException largely a thing of the past if you pay
attention to the warnings.
Practice
Implement SafeMath.SumNumbers(string[] inputs) which:
- Parses each string as an int.
- Skips any string that can't be parsed (do NOT throw).
- Returns the sum of the ones that parsed successfully.
Program.cs calls it with {"10", "x", "20", "y", "5"} and prints exactly:
35
Test your understanding
When should you use an exception instead of a return value to signal failure?
For every possible error
Only when you want to crash the program
When the failure is unusual or unexpected, and forcing every call site to check for it would be noisy
Whenever your method touches a file
Why is a silent catch { } block dangerous?
It's slower than a specific catch
The compiler removes it
It hides bugs by swallowing every exception, even ones you never anticipated
It uses too much memory