Skip to main content

.NET Exception Logging

Exception logging is a crucial aspect of robust application development that helps developers track, analyze, and resolve errors that occur during program execution. In this tutorial, we'll explore how to implement effective exception logging in .NET applications, providing you with the tools to debug issues and improve application reliability.

Introduction to Exception Logging

When an exception occurs in your application, simply catching and handling it might not be enough. Without proper logging, you might miss critical information about what went wrong, making troubleshooting difficult. Exception logging solves this by recording detailed information about exceptions for later analysis.

The key benefits of exception logging include:

  • Preserving error details that would otherwise be lost
  • Creating an audit trail for application health monitoring
  • Providing insights for debugging production issues
  • Enabling proactive problem detection
  • Supporting application maintenance and improvement

Basic Exception Logging Techniques

Using Console for Simple Logging

The simplest form of exception logging uses the console to output exception details:

csharp
try
{
// Code that might throw an exception
int result = 10 / 0; // Will throw DivideByZeroException
}
catch (Exception ex)
{
Console.WriteLine($"Error occurred: {ex.Message}");
Console.WriteLine($"Stack trace: {ex.StackTrace}");

// If there's an inner exception
if (ex.InnerException != null)
{
Console.WriteLine($"Inner exception: {ex.InnerException.Message}");
}
}

Output:

Error occurred: Attempted to divide by zero.
Stack trace: at Program.Main() in C:\Projects\MyApp\Program.cs:line 8

While simple, console logging has significant limitations – the logs disappear when the application exits, and they're not easily searchable or analyzable.

File-Based Logging

A more persistent approach is to log exceptions to a file:

csharp
try
{
// Risky code
var data = File.ReadAllText("nonexistent.txt");
}
catch (Exception ex)
{
LogExceptionToFile(ex);
}

void LogExceptionToFile(Exception ex)
{
string logFilePath = "application_error.log";
string logMessage = $"[{DateTime.Now}] ERROR: {ex.Message}\r\n" +
$"Stack Trace: {ex.StackTrace}\r\n" +
"----------------------------------------\r\n";

File.AppendAllText(logFilePath, logMessage);
}

This creates a persistent log file that can be examined after the application has exited.

Using Logging Libraries

While DIY logging works, dedicated logging libraries offer more features and flexibility.

Microsoft.Extensions.Logging

Microsoft's built-in logging framework provides a standardized way to log exceptions:

csharp
using Microsoft.Extensions.Logging;

public class CustomerService
{
private readonly ILogger<CustomerService> _logger;

public CustomerService(ILogger<CustomerService> logger)
{
_logger = logger;
}

public Customer GetCustomer(int id)
{
try
{
// Database code that might fail
if (id < 0)
throw new ArgumentException("Customer ID cannot be negative");

// More code...
return new Customer();
}
catch (ArgumentException ex)
{
_logger.LogWarning(ex, "Invalid customer ID: {CustomerId}", id);
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to retrieve customer {CustomerId}", id);
throw new CustomerNotFoundException($"Could not find customer with ID {id}", ex);
}
}
}

To use this in a console application:

csharp
using Microsoft.Extensions.Logging;

class Program
{
static void Main()
{
// Create a logger factory
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder
.AddConsole()
.AddDebug();
});

var logger = loggerFactory.CreateLogger<Program>();

try
{
// Some operation that might throw
throw new InvalidOperationException("This is a demonstration exception");
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred during program execution");
}
}
}

Serilog for Structured Logging

Serilog is a popular third-party logging library that provides powerful structured logging capabilities:

csharp
// Install via NuGet: 
// Install-Package Serilog
// Install-Package Serilog.Sinks.Console
// Install-Package Serilog.Sinks.File

using Serilog;

class Program
{
static void Main()
{
// Configure Serilog
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.WriteTo.File("logs/app-.log", rollingInterval: RollingInterval.Day)
.CreateLogger();

try
{
Log.Information("Application starting up");

// Code that might throw
var items = new string[5];
var item = items[10]; // Will throw IndexOutOfRangeException
}
catch (Exception ex)
{
// Structured logging with properties
Log.Error(ex, "Failed to process items {@Exception}", new { ExceptionType = ex.GetType().Name });
}
finally
{
Log.CloseAndFlush(); // Important to flush logs
}
}
}

Best Practices for Exception Logging

1. Log the Complete Exception Details

Always capture as much information as possible:

csharp
try
{
// Code that might fail
}
catch (Exception ex)
{
logger.LogError(ex, "Operation failed with {ErrorType}: {ErrorMessage}",
ex.GetType().Name, ex.Message);

// Include context-specific information
logger.LogError("User {UserId} experienced error during {Operation}",
currentUser.Id, "data processing");
}

2. Include Contextual Information

Context helps immensely when troubleshooting:

csharp
try
{
// Process payment code
}
catch (Exception ex)
{
logger.LogError(ex, "Payment processing failed for Order {OrderId}, Amount: {Amount}, Payment Method: {Method}",
order.Id, order.Amount, order.PaymentMethod);
}

3. Use Different Log Levels Appropriately

csharp
// For debugging
logger.LogDebug("Processing item {ItemId}", item.Id);

// For informational messages
logger.LogInformation("User {Username} logged in successfully", user.Username);

// For warning conditions
logger.LogWarning("Database connection pool reaching capacity: {PoolSize}/{MaxSize}", currentSize, maxSize);

// For error conditions
logger.LogError(ex, "Failed to save customer data");

// For critical errors that require immediate attention
logger.LogCritical(ex, "Application database unavailable");

4. Implement Centralized Exception Handling

For web applications, you can implement a global exception handler:

csharp
// ASP.NET Core example
public class ErrorHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ErrorHandlingMiddleware> _logger;

public ErrorHandlingMiddleware(RequestDelegate next, ILogger<ErrorHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}

public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception in request {Path}", context.Request.Path);

// Return an appropriate response to the client
context.Response.StatusCode = 500;
await context.Response.WriteAsJsonAsync(new { error = "An unexpected error occurred" });
}
}
}

// In Startup.Configure
app.UseMiddleware<ErrorHandlingMiddleware>();

Real-World Example: E-commerce Application

Here's a comprehensive example of exception logging in an e-commerce application:

csharp
public class OrderService
{
private readonly IOrderRepository _repository;
private readonly IPaymentGateway _paymentGateway;
private readonly ILogger<OrderService> _logger;

public OrderService(
IOrderRepository repository,
IPaymentGateway paymentGateway,
ILogger<OrderService> logger)
{
_repository = repository;
_paymentGateway = paymentGateway;
_logger = logger;
}

public async Task<OrderResult> PlaceOrder(Order order, PaymentDetails payment)
{
_logger.LogInformation("Processing order {OrderId} for customer {CustomerId}",
order.Id, order.CustomerId);

try
{
// Validate inventory
foreach (var item in order.Items)
{
try
{
var inventory = await _repository.CheckInventory(item.ProductId);
if (inventory < item.Quantity)
{
_logger.LogWarning("Insufficient inventory for product {ProductId}: requested {Requested}, available {Available}",
item.ProductId, item.Quantity, inventory);
return new OrderResult { Success = false, Error = $"Insufficient inventory for product {item.ProductId}" };
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to check inventory for product {ProductId}", item.ProductId);
throw new OrderProcessingException("Inventory verification failed", ex);
}
}

// Process payment
try
{
var paymentResult = await _paymentGateway.ProcessPayment(payment, order.TotalAmount);
if (!paymentResult.Success)
{
_logger.LogWarning("Payment declined for order {OrderId}: {Reason}",
order.Id, paymentResult.DeclineReason);
return new OrderResult { Success = false, Error = "Payment was declined" };
}

_logger.LogInformation("Payment successful for order {OrderId}, Transaction ID: {TransactionId}",
order.Id, paymentResult.TransactionId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Payment processing failed for order {OrderId}, amount: {Amount}",
order.Id, order.TotalAmount);
throw new OrderProcessingException("Payment processing failed", ex);
}

// Save order
try
{
await _repository.SaveOrder(order);
_logger.LogInformation("Order {OrderId} saved successfully", order.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to save order {OrderId} in database", order.Id);
throw new OrderProcessingException("Order could not be saved", ex);
}

return new OrderResult { Success = true, OrderId = order.Id };
}
catch (OrderProcessingException)
{
// These exceptions are already logged, just re-throw
throw;
}
catch (Exception ex)
{
// Catch any unexpected exceptions
_logger.LogError(ex, "Unexpected error processing order {OrderId}", order.Id);
throw new OrderProcessingException("An unexpected error occurred", ex);
}
}
}

Configuring Logging in Different .NET Project Types

Console Application

csharp
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

class Program
{
static void Main(string[] args)
{
var host = Host.CreateDefaultBuilder(args)
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddConsole();
logging.AddDebug();
logging.AddFile("logs/app-{Date}.log");
})
.Build();

var logger = host.Services.GetRequiredService<ILogger<Program>>();

try
{
logger.LogInformation("Application started");
// Application logic
}
catch (Exception ex)
{
logger.LogError(ex, "Application encountered an error");
}
}
}

ASP.NET Core Web Application

csharp
// In Program.cs
var builder = WebApplication.CreateBuilder(args);

// Configure logging
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.AddDebug();
builder.Logging.AddEventLog();

// For production environments, consider adding more sophisticated logging
if (builder.Environment.IsProduction())
{
builder.Logging.AddJsonConsole();
// Add Application Insights or other production logging
}

var app = builder.Build();

// Configure exception handling middleware
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// Add custom error handling middleware
}

Summary

Effective exception logging is essential for building reliable .NET applications. In this tutorial, we've covered:

  • Basic logging techniques using console and file-based logs
  • Modern logging frameworks like Microsoft.Extensions.Logging and Serilog
  • Best practices for comprehensive and contextual exception logging
  • Implementing centralized exception handling
  • Real-world examples in an e-commerce application
  • Configuring logging for different .NET project types

By implementing proper exception logging, you'll be able to more easily identify, troubleshoot, and resolve issues in your applications, leading to better user experiences and more maintainable code.

Additional Resources

Exercises

  1. Implement basic file logging for a simple console application.
  2. Create a custom middleware for exception handling in an ASP.NET Core application.
  3. Use Serilog to implement structured logging with multiple sinks (console, file, and database).
  4. Build a logging service that captures different levels of logs and contextual information.
  5. Implement a global exception handler that logs exceptions and sends email notifications for critical errors.

Happy coding and logging!



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