.NET
exception handling
best practices
error management
re-throwing exceptions

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.

csharp
1try
2{
3    RiskyOperation();
4}
5catch (Exception ex)
6{
7    Log(ex);
8    throw; // preferred
9}

Incorrect version:

csharp
1catch (Exception ex)
2{
3    Log(ex);
4    throw ex; // resets stack trace
5}

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.

csharp
1try
2{
3    repository.Save(order);
4}
5catch (SqlException ex)
6{
7    throw new OrderPersistenceException("Failed to save order", ex);
8}

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.

csharp
1try
2{
3    var payload = File.ReadAllText(path);
4}
5catch (FileNotFoundException ex)
6{
7    logger.Warn(ex, "Config file is missing");
8    return DefaultConfig();
9}
10catch (UnauthorizedAccessException ex)
11{
12    logger.Error(ex, "Cannot access config file");
13    throw;
14}

This pattern separates recoverable conditions from fatal ones.

Exception filters improve clarity

Use when filters to keep branching logic outside catch bodies.

csharp
1try
2{
3    await client.SendAsync(request);
4}
5catch (HttpRequestException ex) when (IsTransient(ex))
6{
7    logger.Warn(ex, "Transient network issue");
8    throw;
9}

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.

csharp
1catch (OrderPersistenceException ex)
2{
3    return Results.Problem(title: "Order save failed", statusCode: 503, detail: ex.Message);
4}

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 Exception broadly 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.

Course illustration
Course illustration

All Rights Reserved.