Best practices for catching and re-throwing .NET exceptions
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
Catching and re-throwing exceptions in .NET is not just about avoiding crashes. It is about preserving diagnostic context, translating errors at architecture boundaries, and keeping behavior predictable for callers. The most important rule is simple: keep stack traces intact and only transform exceptions when you add real value.
Core Sections
Use throw; to preserve the original stack trace
Inside a catch block, throw; rethrows the same exception while preserving the original stack information. Using throw ex; resets the stack trace and makes production debugging much harder.
Incorrect version:
Wrap only at boundaries and keep InnerException
Sometimes you should wrap an exception to expose a domain-specific message. When you do, always include the original exception as InnerException.
This preserves low-level details while giving upper layers a stable domain exception type.
Catch specific exceptions, not Exception by default
Broad catches can hide bugs and make retry logic unsafe. Catch only the exceptions you can handle meaningfully.
This pattern separates recoverable conditions from fatal ones.
Exception filters improve clarity
Use when filters to keep branching logic outside catch bodies.
Filters make intent explicit and avoid nested condition blocks.
Async and logging guidance
In async code, exceptions propagate through await naturally. Avoid fire-and-forget tasks unless you attach explicit exception handling and logging. Otherwise failures can become silent until they trigger unobserved task events.
Use structured logging with operation ids and correlation ids. A generic message without request context rarely helps during incidents. Also avoid logging the same exception at every layer. Pick one boundary for full error logs and let other layers add concise context.
Practical decision rule
Use this rule in code review:
- Catch and recover if you can continue safely.
- Catch, add context, and rethrow if caller needs a domain-level error.
- Do not catch if you cannot improve handling.
This keeps exception flow simple and prevents defensive clutter.
Layering strategy for large services
In larger systems, error handling should follow layer responsibilities. Infrastructure code can throw technical exceptions with detailed diagnostics. Application services can translate those failures into domain-level exceptions meaningful to upstream callers. API layers then map domain exceptions to HTTP responses or user-facing error contracts.
This layered policy prevents low-level details from leaking to clients while keeping enough context in logs for debugging. Document the mapping rules in one place so teams do not invent inconsistent patterns across endpoints.
Add unit tests for exception mapping behavior, not only happy paths. Tests should assert both the thrown type and preservation of InnerException so future refactors do not accidentally remove diagnostic detail.
Common Pitfalls
- Using
throw ex;and losing the original stack trace. - Wrapping every exception regardless of layer or added context.
- Catching
Exceptionbroadly and accidentally swallowing programming errors. - Logging the same exception many times across layers.
- Ignoring async fire-and-forget task failures.
Summary
- Prefer
throw;when rethrowing from the same catch block. - Wrap exceptions only when crossing boundaries or adding meaningful context.
- Preserve root-cause details through
InnerException. - Catch specific exception types you can actually handle.
- Keep exception logging structured, contextual, and non-duplicative.

