Mastering Exception Handling in C#

Learn how to handle exceptions like a pro in C#, with modern techniques that simplify error handling, improve maintainability, and ensure your applications run smoothly.

F. I.
.NET Insights: C# and ASP.NET Core
5 min readSep 5, 2024

--

Handling exceptions is a crucial part of writing robust and maintainable applications in C#. Yet, so many developers still fall into traps that lead to hard to debug code and loss of important error information. In this blog, we’ll try to cover the best practices for exception handling in C# and introduce modern, reusable techniques that will make your code cleaner, more efficient, and easier to maintain.

Not a Medium member? You can still read the full blog here.

Why Exception Handling Matters

Exceptions in C# are runtime errors that occur when your program encounters unexpected conditions like invalid input, network failures, or resource constraints. If not handled properly, exceptions can cause your application to crash, degrade the user experience, and make debugging a nightmare.

Proper exception handling is essential for:

  • Stability: Preventing unexpected crashes and ensuring graceful error handling.
  • Debugging: Preserving useful information (like stack traces) to narrow down the root cause of any issue.
  • Security: Avoiding to expose sensitive information in error messages.

Best Practices for Exception Handling in C#

1. Catch Only What You Can Handle

A common mistake developers do is catching exceptions without handling them properly. If don’t know how to handle an exception inside a method in a meaningful way, it’s far better to let it bubble up to a higher level. Catch the exceptions only where you can act on.

try
{
// Code that might throw an exception
}
catch (SpecificException ex)
{
// Handle specific exception, like logging or retrying
}

Avoid: Catching general exceptions unless it’s at the top of your call stack, such as in your global error handling code. Catching everything indiscriminately makes it harder to diagnose specific problems.

2. Rethrow Exceptions Correctly: Avoid throw ex;

Another common mistakes in exception handling is usingthrow ex; inside a catch block. Many developers don’t know that this resets the stack trace, making it much harder to track where the exception originally raised from. Always use throw; to preserve the original stack trace.

Incorrect:

catch (Exception ex)
{
// This resets the stack trace
throw ex;
}

Correct:

catch (Exception ex)
{
// Preserves the original stack trace
throw;
}

By preserving the stack trace, you maintain the most critical information about where the error originated, making it much easier to debug and fix the problem.

3. Use Custom Exceptions for Domain-Specific Errors

If built-in exceptions like ArgumentNullException or InvalidOperationException etc. don’t accurately describe an error in your application, consider creating your custom exceptions. This adds more clarity and makes your code much readable.

public class InvalidOrderException : Exception
{
public InvalidOrderException(string message) : base(message) { }
}

Use custom exceptions only when they add real value to understanding the problem in your specific domain.

4. Log Exceptions, but Avoid Overlogging

Logging exceptions is essential, but overlogging can overwhelm your logs with unnecessary data. Log exceptions only when they provide valuable insight, and make sure not to expose sensitive information.

catch (Exception ex)
{
// Log the exception with meaningful details
logger.LogError(ex, "Error occurred during operation.");
throw; // Rethrow the exception to maintain the stack trace
}

Avoid: Logging every minor exception or logging too much detail, which can flood your logs and obscure the real issues. Overlogging can also hurt the application performance.

5. Centralized Exception Handling with Filters in ASP.NET Core

If you’re building ASP.NET Core applications, you can centralize your exception handling using exception filters. This is a modern way to handle errors across your entire application in one place.

Example: Global Exception Filter

public class GlobalExceptionFilter : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
var exception = context.Exception;
// Handle and log the exception globally
context.Result = new ObjectResult("An internal error occurred")
{
StatusCode = 500
};
context.ExceptionHandled = true;
}
}

Register this filter globally in Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(options =>
{
options.Filters.Add<GlobalExceptionFilter>();
});
}

This ensures that all exceptions are handled consistently across your application, improving the overall application’s maintainability.

Modern Exception Handling Techniques

1. Use Functional Programming for Cleaner Exception Handling

Functional programming principles can help you write cleaner, more readable code. You can encapsulate exception handling using higher-order functions, which reduces boilerplate and makes your logic more declarative.

Using this approach can be a personal choice as many developers may not be comfortable using it.

public static class Try
{
public static void Execute(Action action, Action<Exception> handleError)
{
try
{
action();
}
catch (Exception ex)
{
handleError(ex);
}
}
}

Usage:

Try.Execute(() => 
{
// Risky operation
int result = 10 / 0;
}, ex => Console.WriteLine($"Error: {ex.Message}"));

This keeps your code clean, concise, and focused on business logic.

2. Handle Asynchronous Exceptions Gracefully

When dealing with asynchronous code, exception handling can become rather trickier. Use Task.Run or create async wrappers to manage async exceptions effectively.

public static async Task SafeExecuteAsync(Func<Task> action, Action<Exception> handleError)
{
try
{
await action();
}
catch (Exception ex)
{
handleError(ex);
}
}

Usage:

await SafeExecuteAsync(async () =>
{
await SomeAsyncOperation();
}, ex => Console.WriteLine($"Async error: {ex.Message}"));

This approach ensures that exceptions in async code are handled just as gracefully as in synchronous code.

3. Use Result Wrappers for Explicit Error Handling

For more predictable error flows, use a Result wrapper to explicitly handle success or failure, making it clear when an operation fails without relying solely on exceptions.

This is the approach that I personally like.

public class Result<T>
{
public T Value { get; }
public Exception Error { get; }
public bool IsSuccess => Error == null;

private Result(T value, Exception error)
{
Value = value;
Error = error;
}

public static Result<T> Success(T value) => new Result<T>(value, null);
public static Result<T> Failure(Exception error) => new Result<T>(default, error);
}

Usage:

var result = Divide(10, 0);
if (result.IsSuccess)
{
Console.WriteLine($"Result: {result.Value}");
}
else
{
Console.WriteLine($"Error: {result.Error.Message}");
}

This pattern provides a structured way to manage errors without depending exclusively on exceptions.

Conclusion: Write Robust, Modern C# Code

Mastering exception handling is more than just avoiding application to crash. It’s more about writing maintainable, clear, and resilient code that deals with unexpected conditions gracefully. By applying these best practices and modern techniques — such as leveraging functional programming principles, handling asynchronous errors correctly, and centralizing exception handling in ASP.NET Core , you can significantly improve not only your code quality but also overall stability of your applications.

Remember, proper exception handling not only helps you debug faster but also ensures a better user experience.

Final Thoughts

What exception-handling strategies have worked best for you? Have you tried using modern techniques like Result wrappers or higher-order functions? Share your thoughts and experiences in the comments below!

If you found this article helpful and want more insights, be sure to follow Faisal Iqbal for regular updates on .NET and ASP.NET Core.

For those who want to dive deeper into these topics, check out my publication, .NET Insights: C# and ASP.NET Core, where we share tutorials, expert advice, and the latest trends in modern web development. Stay tuned for more!

--

--

F. I.
.NET Insights: C# and ASP.NET Core

Writes about event-driven architectures, distributed systems, garbage collection and other topics related to .NET and ASP.NET.