Exceptions and error handling
Catch what you can handle, filter with when, define custom exceptions that carry data, and choose between exceptions and result types.
Throwing and catching
// custom exceptions: three standard constructors, plus your own data
public sealed class InsufficientStockException : Exception
{
public InsufficientStockException(string sku, int requested, int available)
: base($"SKU {sku}: requested {requested}, available {available}")
{
Sku = sku;
Requested = requested;
Available = available;
}
public InsufficientStockException(string message, Exception inner) : base(message, inner) { }
public string Sku { get; }
public int Requested { get; }
public int Available { get; }
}
// throw with a filter so you only catch what you understand
try
{
PlaceOrder("ABC-1", 5);
}
catch (InsufficientStockException ex) when (ex.Available > 0)
{
Console.WriteLine($"partial: {ex.Available} available");
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"bad state: {ex.Message}");
}
finally
{
// always runs; release the connection, close the file
}
static void PlaceOrder(string sku, int qty) => throw new InsufficientStockException(sku, qty, 2);- Let exceptions propagate unless you can do something about them. Logging and rethrowing at every level produces noise and hides the real handler.
throw;preserves the original stack trace;throw ex;resets it to the current line and destroys the evidence.- A
whenfilter runs before the stack unwinds, so it is cheap and leaves the original trace intact even when the filter rejects. - Exceptions are objects with allocation and unwinding cost. Do not use them for ordinary control flow that happens per item in a loop.
// filter on data that only a real handler can know
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.ServiceUnavailable)
{
await Task.Delay(TimeSpan.FromSeconds(1));
return await Retry(...); // this caller can actually recover
}
// never do this: it hides bugs and text-matches on messages
catch (Exception ex)
{
if (ex.Message.Contains("timeout")) return null;
throw;
}Exceptions versus result types
| Situation | Model | Why |
|---|---|---|
| Bad argument from a programmer error | Throw | Should never happen; fail loudly in development |
| Expected domain outcome (insufficient stock) | Result or a typed value | Callers must handle it; it is not exceptional |
| Validation of user input | Return a list of errors | Multiple problems at once, all reportable |
| I/O or network failure | Throw, then catch at the boundary | The infrastructure choice belongs in one place |
| Cleanup that must not fail | finally or a using declaration | The language guarantees it runs |
// a small result type: no exceptions, no exceptions-to-control-flow
public readonly record struct Result<T>(T? Value, string? Error)
{
public bool IsSuccess => Error is null;
public static Result<T> Ok(T value) => new(value, null);
public static Result<T> Fail(string error) => new(default, error);
}
public Result<int> Reserve(string sku, int qty)
{
int available = Stock(sku);
if (qty <= 0) return Result<int>.Fail("quantity must be positive");
if (qty > available) return Result<int>.Fail($"only {available} available");
return Result<int>.Ok(qty);
}
var result = Reserve("ABC-1", 5);
if (!result.IsSuccess)
Console.WriteLine(result.Error);
static int Stock(string sku) => 2;
// ArgumentException family is the idiomatic guard for programmer errors
static void Configure(string name)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
}⚠️
Do not catch
Exception to keep a service alive. An OutOfMemoryException or a StackOverflowException (which cannot be caught at all) leaves the process in a state you cannot reason about. Catch specific types, and let a top-level handler log and terminate.FAQ
Is catching an exception slow?
Throwing and unwinding is expensive in the order of microseconds; entering a try block costs nothing when no exception is thrown. The right question is not speed but whether the condition is truly exceptional.
Should I rethrow with throw or throw ex?
Always
throw;. throw ex; resets StackTrace to the current frame, so the log tells you where you rethrew rather than where it went wrong.Related
LINQ and async/await File I/O, streams and JSON serialization
Last refreshed 2026-09-18.