Every way to handle failure
Exceptions, try/catch/finally, the Try pattern, null, result objects, retries and ProblemDetails. How code says something went wrong, and who deals with it.
Split failures into two piles first.
Expected outcomes: a typo in a form, a missing key, one bad line in an upload. They happen in normal use and the caller has a sensible response. Report them in the return value:
- only one way to fail, no reason needed → return
nullfrom a method typedT?; - the caller wants a yes/no and the value → a Try method,
bool TryParse(string s, out int value); - the caller needs to know why → a small result object with the value or an error message.
Broken assumptions: a null argument, a call in the wrong
state, a settings file your team shipped that won't parse, a
database that's gone. Throw an exception, and catch it only where
you can do something useful: retry, show a message, or log it and
return a 500. Everywhere else, let it travel.
At the edges of a program the rule stays the same but the shape changes. A web API turns failures into status codes, with a ProblemDetails body. An HTTP call to someone else's server gets timeouts and retries for failures that are likely to go away by themselves.
Exceptions
throw stops the method, and the exception climbs the call stack until something catches it.
Formal. When an exception is thrown, the runtime searches the call
stack from the throwing method outward for a try whose catch
clauses match the exception's type (and whose when filter, if any, is
true). Within one try, catch clauses are tested top to bottom and the
first match wins. Every frame between the throw and that handler is
abandoned, and the finally blocks of those frames run as control
leaves them. The search, including any when filters, happens before
the inner finally blocks run. With no matching handler the exception
is unhandled: the runtime prints it with its stack trace and ends the
process.
In plain English. A thrown exception is like an alarm going off in
the method that's running. That method stops. If it has no plan for
that alarm, the method that called it stops too, and so on up the chain
of calls, until one of them has a catch for that kind of alarm. Any
finally cleanup along the way still runs. If no method has a plan,
the whole program stops and prints the alarm and the chain of calls.
Example. Main calls LoadOrders, which calls ParseLine, which
throws FormatException. If only Main has
catch (FormatException), both ParseLine and LoadOrders stop at
once and Main's catch runs next.
Why this rule exists. The method that detects a problem is often not the one that knows what to do about it; propagation lets the failure reach the code that can decide.
The nearest matching catch up the call stack handles it; finally blocks run on the way.
Section
try, catch and finally
Catch a specific exception type, and clean up in finally whatever happens.
Your own exception type when callers need to catch your failure specifically.
Formal. Inside a catch block, a bare throw; rethrows the caught
exception with its original stack trace intact. throw ex; throws the
same object as if it were new, so its stack trace restarts at the
rethrow and the frames where it originated are lost; the .NET analyzers
report this as warning CA2200. To add context, throw a new exception
and pass the original as its InnerException.
In plain English. A stack trace is the record of where an error
started. throw; passes the error on with that record untouched.
throw ex; wipes the record and makes it look as if the error started
in your catch block, which sends whoever debugs it to the wrong place.
Example. catch (Exception ex) { Log(ex); throw; } keeps the
original line numbers. catch (IOException ex) { throw new ImportException("Couldn't read orders.csv", ex); } adds a message and
keeps the original inside it.
Why this rule exists. The origin of a failure is the single most useful fact for fixing it.
throw; keeps the original stack trace; throw ex; throws it away.
The catches that hide bugs, and the one place near the top where catching everything is right.
Failure as a return value
The first time the course meets the choice, with user input.
Three ways to report an expected failure, and the rule of thumb for throw or return.
In async code
Section
Errors in async code
An exception travels through await, and Task.WhenAll can collect several at once.
Across the network
For failures likely to fix themselves, and the standard resilience handler that wraps them up.
Section
Status codes and ProblemDetails
An API reports failure with an honest status code and a ProblemDetails body, never a 200 with an error message.