Skip to main content

.NET Global Exception Handling

In software development, errors are inevitable. However, the way we handle these errors can make a significant difference in the robustness and user experience of our applications. While we've already explored the basics of exception handling in .NET, this guide will focus on implementing global exception handling strategies that can catch and manage errors across your entire application.

What is Global Exception Handling?

Global exception handling refers to a centralized approach to catching and processing exceptions that occur anywhere in your application. Rather than scattering try-catch blocks throughout your code, you establish a "safety net" that catches any unhandled exceptions, allowing you to:

  • Log exceptions consistently
  • Display user-friendly error messages
  • Take recovery actions when possible
  • Notify developers or support teams
  • Maintain a better user experience despite errors

Let's explore different approaches to global exception handling in various .NET application types.

Global Exception Handling in Console Applications

Even console applications can benefit from global exception handling. The AppDomain.CurrentDomain.UnhandledException event can catch exceptions that would otherwise crash your application.

csharp
using System;

class Program
{
static void Main(string[] args)
{
// Register the global exception handler
AppDomain.CurrentDomain.UnhandledException += GlobalExceptionHandler;

Console.WriteLine("Application started. Enter a number to divide 100 by:");
try
{
string input = Console.ReadLine();
int divisor = int.Parse(input);

// This might throw a DivideByZeroException
double result = 100 / divisor;
Console.WriteLine($"Result: {result}");
}
catch (FormatException)
{
Console.WriteLine("That's not a valid number.");
}

// This will trigger our global handler since there's no local catch
Console.WriteLine("Now triggering an unhandled exception...");
throw new ApplicationException("This is a test exception");
}

private static void GlobalExceptionHandler(object sender, UnhandledExceptionEventArgs e)
{
Exception ex = (Exception)e.ExceptionObject;
Console.WriteLine("\n*** GLOBAL EXCEPTION HANDLER ***");
Console.WriteLine($"An unhandled exception occurred: {ex.Message}");
Console.WriteLine($"Stack trace: {ex.StackTrace}");

// Log the exception to a file, database, or monitoring service
// LogException(ex);

Console.WriteLine("The application will now shut down.");

// In a real application, you might want to:
// 1. Log detailed information about the exception
// 2. Notify administrators
// 3. Try to save user data if applicable
}
}

Output:

Application started. Enter a number to divide 100 by:
5
Result: 20
Now triggering an unhandled exception...

*** GLOBAL EXCEPTION HANDLER ***
An unhandled exception occurred: This is a test exception
Stack trace: [Stack trace information would appear here]
The application will now shut down.

Note that while this approach catches unhandled exceptions, the application will still terminate after the handler executes. This is suitable for logging and cleanup operations before shutdown.

Global Exception Handling in ASP.NET Core Web Applications

ASP.NET Core provides several built-in mechanisms for global exception handling. Let's explore the most common approaches:

1. Using Exception Handling Middleware

The most flexible approach is using the exception handling middleware, which catches exceptions thrown during the processing of HTTP requests.

csharp
public class Startup
{
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// Exception handling middleware must be the first middleware component added
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
context.Response.StatusCode = 500;
context.Response.ContentType = "application/json";

var exceptionHandlerPathFeature =
context.Features.Get<IExceptionHandlerPathFeature>();

var exception = exceptionHandlerPathFeature?.Error;

// Log the exception
// logger.LogError(exception, "Unhandled exception");

var response = new
{
error = "An error occurred while processing your request.",
detail = env.IsDevelopment() ? exception?.Message : null
};

await context.Response.WriteAsync(
JsonSerializer.Serialize(response));
});
});

// Other middleware components
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}

2. Using Custom Exception Filter in MVC/API Controllers

For ASP.NET Core MVC or API applications, you can create a custom exception filter:

csharp
public class GlobalExceptionFilter : IExceptionFilter
{
private readonly ILogger<GlobalExceptionFilter> _logger;
private readonly IWebHostEnvironment _env;

public GlobalExceptionFilter(
ILogger<GlobalExceptionFilter> logger,
IWebHostEnvironment env)
{
_logger = logger;
_env = env;
}

public void OnException(ExceptionContext context)
{
_logger.LogError(context.Exception, "Unhandled exception");

var response = new
{
error = "An error occurred while processing your request.",
detail = _env.IsDevelopment() ? context.Exception.Message : null
};

context.Result = new JsonResult(response)
{
StatusCode = StatusCodes.Status500InternalServerError
};

context.ExceptionHandled = true;
}
}

Register this filter globally in your Startup.cs:

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

3. Using Problem Details for HTTP APIs

ASP.NET Core provides a standardized format for error responses called Problem Details (RFC 7807). This creates consistent error response formats:

csharp
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = null;
})
.ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
var problemDetails = new ValidationProblemDetails(context.ModelState)
{
Title = "One or more model validation errors occurred.",
Status = StatusCodes.Status400BadRequest,
Instance = context.HttpContext.Request.Path
};

return new BadRequestObjectResult(problemDetails);
};
});
}

Global Exception Handling in WPF Applications

Windows Presentation Foundation (WPF) applications can implement global exception handling using the Application class:

csharp
public partial class App : Application
{
public App()
{
this.DispatcherUnhandledException += App_DispatcherUnhandledException;
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
}

private void App_DispatcherUnhandledException(object sender,
System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
{
// Handle UI thread exceptions
string errorMessage = $"An unexpected error occurred: {e.Exception.Message}";
MessageBox.Show(errorMessage, "Error", MessageBoxButton.OK, MessageBoxImage.Error);

// Log the exception
// LogException(e.Exception);

// Prevent application from crashing
e.Handled = true;
}

private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
// Handle exceptions not caught by the UI thread
Exception ex = (Exception)e.ExceptionObject;
MessageBox.Show($"Fatal exception: {ex.Message}", "Fatal Error",
MessageBoxButton.OK, MessageBoxImage.Error);

// Log the exception
// LogException(ex);
}

private void TaskScheduler_UnobservedTaskException(object sender,
UnobservedTaskExceptionEventArgs e)
{
// Handle exceptions from Tasks
MessageBox.Show($"Task exception: {e.Exception.Message}", "Task Error",
MessageBoxButton.OK, MessageBoxImage.Error);

// Log the exception
// LogException(e.Exception);

e.SetObserved(); // Prevent application from crashing
}
}

Best Practices for Global Exception Handling

Regardless of application type, follow these best practices:

1. Create a Centralized Logger

Implement a centralized logging system to record exception details:

csharp
public class ExceptionLogger
{
private readonly ILogger _logger;

public ExceptionLogger(ILogger logger)
{
_logger = logger;
}

public void LogException(Exception ex, string additionalInfo = null)
{
// Build structured logging data
var logData = new
{
ExceptionType = ex.GetType().Name,
Message = ex.Message,
StackTrace = ex.StackTrace,
InnerException = ex.InnerException?.Message,
AdditionalInfo = additionalInfo,
Timestamp = DateTime.UtcNow
};

_logger.LogError(ex, "Exception occurred: {ExceptionDetails}",
JsonSerializer.Serialize(logData));
}
}

2. Different Error Handling for Different Audiences

Provide appropriate error details based on the environment:

csharp
public string GetErrorResponse(Exception ex, bool isDevelopment)
{
if (isDevelopment)
{
return $"Error: {ex.Message}\nStack Trace: {ex.StackTrace}";
}
else
{
// For production: don't expose sensitive details
return "An unexpected error occurred. Please contact support.";
}
}

3. Create Custom Exception Types

For domain-specific exceptions, create custom types:

csharp
public class OrderProcessingException : Exception
{
public string OrderId { get; }
public OrderProcessingStage FailureStage { get; }

public OrderProcessingException(string orderId, OrderProcessingStage stage,
string message, Exception innerException = null)
: base(message, innerException)
{
OrderId = orderId;
FailureStage = stage;
}
}

public enum OrderProcessingStage
{
Validation,
Payment,
Inventory,
Shipping
}

4. Implement a Global Exception Handler Class

Create a reusable exception handler:

csharp
public class GlobalExceptionHandler
{
private readonly ILogger _logger;
private readonly INotificationService _notificationService;
private readonly IErrorReportGenerator _reportGenerator;

public GlobalExceptionHandler(
ILogger logger,
INotificationService notificationService,
IErrorReportGenerator reportGenerator)
{
_logger = logger;
_notificationService = notificationService;
_reportGenerator = reportGenerator;
}

public async Task HandleExceptionAsync(Exception ex, string context)
{
// 1. Log the exception
_logger.LogError(ex, $"Exception in context: {context}");

// 2. Generate error report
var report = _reportGenerator.CreateReport(ex, context);

// 3. Send notification for critical errors
if (IsCriticalException(ex))
{
await _notificationService.NotifyTeamAsync(report);
}

// 4. Track exception metrics
IncrementExceptionMetric(ex.GetType().Name);
}

private bool IsCriticalException(Exception ex)
{
// Logic to determine if an exception requires immediate attention
return ex is OutOfMemoryException ||
ex is StackOverflowException ||
ex is AccessViolationException;
}

private void IncrementExceptionMetric(string exceptionTypeName)
{
// Increment exception counter in metrics system
// MetricsRecorder.IncrementCounter($"Exceptions.{exceptionTypeName}");
}
}

Real-World Example: E-commerce Order Processing System

Let's see how global exception handling works in a real-world e-commerce scenario:

csharp
// Program.cs or Startup.cs
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);

// Add services
builder.Services.AddControllers();
builder.Services.AddSingleton<IOrderService, OrderService>();
builder.Services.AddSingleton<IExceptionLogger, ExceptionLogger>();

// Add global exception handling
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();

var app = builder.Build();

// Configure middleware pipeline
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.MapControllers();

app.Run();
}
}

// GlobalExceptionHandler.cs
public class GlobalExceptionHandler : IExceptionHandler
{
private readonly ILogger<GlobalExceptionHandler> _logger;
private readonly IHostEnvironment _environment;

public GlobalExceptionHandler(
ILogger<GlobalExceptionHandler> logger,
IHostEnvironment environment)
{
_logger = logger;
_environment = environment;
}

public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
_logger.LogError(exception, "Unhandled exception");

int statusCode;
string title;

// Different handling based on exception type
if (exception is OrderNotFoundException)
{
statusCode = StatusCodes.Status404NotFound;
title = "Order not found";
}
else if (exception is PaymentProcessingException)
{
statusCode = StatusCodes.Status400BadRequest;
title = "Payment processing error";
}
else if (exception is UnauthorizedAccessException)
{
statusCode = StatusCodes.Status403Forbidden;
title = "Access denied";
}
else
{
statusCode = StatusCodes.Status500InternalServerError;
title = "An unexpected error occurred";
}

var problemDetails = new ProblemDetails
{
Status = statusCode,
Title = title,
Instance = httpContext.Request.Path,
Detail = _environment.IsDevelopment() ? exception.ToString() : null
};

httpContext.Response.StatusCode = statusCode;
httpContext.Response.ContentType = "application/problem+json";

await JsonSerializer.SerializeAsync(
httpContext.Response.Body,
problemDetails);

return true;
}
}

// OrdersController.cs
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly IOrderService _orderService;

public OrdersController(IOrderService orderService)
{
_orderService = orderService;
}

[HttpGet("{id}")]
public async Task<ActionResult<Order>> GetOrder(string id)
{
// No need for try-catch as global handler will catch exceptions
var order = await _orderService.GetOrderAsync(id);
return Ok(order);
}

[HttpPost]
public async Task<ActionResult<Order>> CreateOrder(OrderRequest request)
{
var order = await _orderService.CreateOrderAsync(request);
return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, order);
}
}

// Custom exception types
public class OrderNotFoundException : Exception
{
public string OrderId { get; }

public OrderNotFoundException(string orderId)
: base($"Order with ID {orderId} was not found")
{
OrderId = orderId;
}
}

public class PaymentProcessingException : Exception
{
public string PaymentId { get; }

public PaymentProcessingException(string paymentId, string message)
: base(message)
{
PaymentId = paymentId;
}
}

In this example:

  • We set up a global exception handler that catches all unhandled exceptions
  • Different exception types are mapped to appropriate HTTP status codes
  • Detailed errors are shown in development, but hidden in production
  • All exceptions are centrally logged

Summary

Global exception handling is a crucial aspect of robust application design. By implementing proper global exception handling strategies, you can:

  • Create a better user experience by showing appropriate error messages
  • Maintain consistent error handling logic across your application
  • Centralize logging and monitoring of exceptions
  • Improve application reliability and maintainability

Remember that while global exception handling serves as a safety net, it doesn't replace proper exception handling in critical sections of your code. Use both approaches together: handle specific exceptions where appropriate, and rely on global handlers to catch anything unexpected.

Additional Resources

Practice Exercises

  1. Create a console application that demonstrates global exception handling using AppDomain.UnhandledException.

  2. Build a simple ASP.NET Core web API with global exception handling that catches and appropriately handles different exception types.

  3. Implement a logging system that records exceptions to a file and sends email notifications for critical errors.

  4. Create a WPF application with global error handling that shows user-friendly error messages and logs exceptions.

  5. Design a custom exception hierarchy for a domain-specific application (e.g., banking, e-commerce, or healthcare).



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)