Catching and Rethrowing Exceptions in C#: Best Practices and Anti-Patterns

Nov 22, 2025 · Programming · 8 views · 7.8

Keywords: C# | Exception Handling | Stack Trace | Logging | Anti-Pattern

Abstract: This article provides an in-depth analysis of catching and rethrowing exceptions in C#. It examines common code examples, explains the problem of losing stack trace information when using throw ex, and contrasts it with the correct usage of throw to preserve original exception details. The discussion covers appropriate applications in logging, exception wrapping, and specific exception handling scenarios, along with methods to avoid the catch-log-rethrow anti-pattern, helping developers write more robust and maintainable code.

Fundamentals of Exception Handling

In C# programming, exception handling is a crucial mechanism for ensuring program robustness. Through try-catch blocks, developers can catch and handle runtime errors, preventing unexpected program termination. However, inappropriate exception handling approaches can introduce new problems and even obscure the true root cause of errors.

Problematic Code Analysis

Consider the following code example that demonstrates a common mistake:

public static string SerializeDTO(DTO dto) {
    try {
        XmlSerializer xmlSer = new XmlSerializer(dto.GetType());
        StringWriter sWriter = new StringWriter();
        xmlSer.Serialize(sWriter, dto);
        return sWriter.ToString();
    }
    catch(Exception ex) {
        throw ex;
    }
}

This code attempts to "handle" exceptions through try-catch, but in reality, it simply rethrows the caught exception. The core issue with this approach is that the throw ex statement resets the exception's stack trace information, making it impossible to trace back to the original location where the exception occurred during debugging.

Correct Rethrowing Approach

If you genuinely need to rethrow an exception, you should use the parameterless throw statement:

try {
    // Code that might throw exceptions
}
catch(Exception ex) {
    throw;  // Preserves original stack trace
}

This approach maintains the complete call stack of the exception, providing valuable information for problem diagnosis.

Legitimate Exception Handling Scenarios

While simple catch-and-rethrow typically adds no value, there are specific scenarios where this pattern is justified:

Logging

Recording error information before an exception propagates upward is a common requirement:

try {
    // Business logic code
}
catch(Exception ex) {
    logger.Error($"Operation failed with parameters: {param1}, {param2}", ex);
    throw;
}

Exception Wrapping

In certain architectures, you might need to wrap underlying exceptions into domain-specific exceptions:

try {
    // Data access code
}
catch(SqlException ex) {
    throw new DataAccessException("Database operation failed", ex);
}

Specific Exception Handling

Implement different handling strategies for different types of exceptions:

try {
    // File operations
}
catch(FileNotFoundException ex) {
    // Create default file
    CreateDefaultFile();
}
catch(IOException ex) {
    // Log and rethrow
    logger.Error("IO operation failed", ex);
    throw;
}

Avoiding Anti-Patterns

The catch-log-rethrow pattern mentioned in reference articles is generally considered an anti-pattern for several reasons:

Better Solutions

For requirements that need to log contextual information when exceptions occur, consider the following alternatives:

Global Exception Handling

Set up global exception handlers at application entry points or middleware:

// In ASP.NET Core
app.UseExceptionHandler(errorApp => {
    errorApp.Run(async context => {
        var exceptionHandler = context.Features.Get<IExceptionHandlerFeature>();
        // Log exception with context information
        await LogExceptionWithContext(exceptionHandler.Error, context);
    });
});

Aspect-Oriented Programming (AOP)

Use AOP frameworks like PostSharp for centralized management of cross-cutting concerns:

[Serializable]
public class ExceptionLoggingAttribute : OnExceptionAspect {
    public override void OnException(MethodExecutionArgs args) {
        var logger = LogManager.GetLogger(args.Method.DeclaringType);
        logger.Error($"Method {args.Method.Name} execution failed", args.Exception);
    }
}

Performance Considerations

While exception handling incurs some performance overhead, on modern .NET platforms, this cost is generally acceptable. Key considerations include:

Conclusion

In C# exception handling, simple catch-and-rethrow is typically unnecessary, especially when using throw ex which destroys stack trace information. The correct approach depends on specific requirements: either let exceptions propagate naturally, use throw to preserve stack information, or implement appropriate logging, exception wrapping, or specific handling when needed. Through mechanisms like global exception handling or AOP, cross-cutting concerns can be better managed, maintaining code clarity and maintainability.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.