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 when filter 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

SituationModelWhy
Bad argument from a programmer errorThrowShould never happen; fail loudly in development
Expected domain outcome (insufficient stock)Result or a typed valueCallers must handle it; it is not exceptional
Validation of user inputReturn a list of errorsMultiple problems at once, all reportable
I/O or network failureThrow, then catch at the boundaryThe infrastructure choice belongs in one place
Cleanup that must not failfinally or a using declarationThe 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.

LINQ and async/await File I/O, streams and JSON serialization

Last refreshed 2026-09-18.