How does exception handling work in C#, and what are the best practices?
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;vsthrow 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
usingoverfinallyfor 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.
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
}