How does exception handling work in C#, and what are the best practices?

C# Error Handling Intermediate 1–2 Yrs2–3 Yrs3–5 Yrs Conceptual 2 min read

Quick Answer

try runs code that might fail, catch handles specific exception types, finally always runs (cleanup). Best practices: catch the most specific type you can handle, don't swallow exceptions, use throw; (not throw ex;) to rethrow and keep the stack trace, and let exceptions you can't handle bubble to a central handler.

try/catch/finally — catch runs for matching exception types, finally always runs for cleanup. The rules I follow: catch the most specific exception I can actually do something about, never swallow exceptions silently, and when I rethrow, use bare throw; so the original stack trace survives — throw ex; wipes it. I don't use exceptions for expected outcomes; that's what TryParse-style methods are for. For a web app I let unhandled exceptions bubble up to a single piece of exception-handling middleware that logs them and returns a consistent error body, rather than try/catch in every controller.

  • throw; vs throw ex; — throw; preserves the original stack trace; throw ex; resets it to the current line, hiding where the error really happened.
  • Exception filters — catch (SqlException ex) when (ex.Number == 1205) — catch only matching cases without re-throwing.
  • Don't use exceptions for control flow — they're expensive; use TryParse-style patterns for expected failures.
  • Custom exceptions — derive from Exception, add context properties, provide the standard constructors. Throw them for domain errors that callers might handle differently.
  • Cleanup — prefer using over finally for disposal.
  • Central handling — in ASP.NET Core, exception-handling middleware turns unhandled exceptions into a consistent response; don't wrap every action in try/catch.
  • Log the exception once, with context, where you actually handle it — not at every layer.

C# — rethrow + filter
try
{
    await _payments.ChargeAsync(order);
}
catch (PaymentDeclinedException)
{
    return BadRequest("Card declined");     // handled
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests)
{
    _logger.LogWarning(ex, "Payment gateway throttled");
    throw;                                   // preserves stack trace
}