Skip to content
Free public beta until December 4, 2026: every course is open, no card needed. After that, the first chapter of each course stays free and full access is a one-time $39 ($19 for up to 100 founding members chosen for helpful feedback). Your progress is kept. How the beta works

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 null from a method typed T?;
  • 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.

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

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.

An API reports failure with an honest status code and a ProblemDetails body, never a 200 with an error message.