Overview
Understanding exception handling deeply is critical for building production-ready applications. This includes knowing when to catch, when to throw, how to create custom exceptions, and how to implement global error handling in ASP.NET Core.
Core Concepts
Try-Catch-Finally Patterns
// Basic pattern
public void ProcessFile(string path)
{
FileStream file = null;
try
{
file = File.OpenRead(path);
// Process file
}
catch (FileNotFoundException ex)
{
_logger.LogWarning(ex, "File not found: {Path}", path);
// Handle missing file
}
catch (IOException ex)
{
_logger.LogError(ex, "IO error reading file: {Path}", path);
throw; // Can't handle, let caller deal with it
}
finally
{
// Always runs - cleanup
file?.Dispose();
}
}
// Modern using statement (preferred)
public void ProcessFileModern(string path)
{
try
{
using var file = File.OpenRead(path); // Auto-disposes
// Process file
}
catch (FileNotFoundException ex)
{
_logger.LogWarning(ex, "File not found: {Path}", path);
}
}
Custom Exceptions
// Domain-specific exception
public class InsufficientFundsException : Exception
{
public decimal CurrentBalance { get; }
public decimal RequestedAmount { get; }
public InsufficientFundsException(decimal currentBalance, decimal requestedAmount)
: base($"Insufficient funds. Balance: {currentBalance:C}, Requested: {requestedAmount:C}")
{
CurrentBalance = currentBalance;
RequestedAmount = requestedAmount;
}
// Required for serialization
protected InsufficientFundsException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context)
: base(info, context)
{
}
}
// Usage
public class BankAccount
{
public decimal Balance { get; private set; }
public void Withdraw(decimal amount)
{
if (amount > Balance)
throw new InsufficientFundsException(Balance, amount);
Balance -= amount;
}
}
// Caller
try
{
account.Withdraw(1000m);
}
catch (InsufficientFundsException ex)
{
_logger.LogWarning(
"Withdrawal failed. Balance: {Balance}, Requested: {Amount}",
ex.CurrentBalance,
ex.RequestedAmount);
// Show user-friendly message
return BadRequest($"Insufficient funds. Available: {ex.CurrentBalance:C}");
}
Exception Filters (when keyword)
// Catch only when condition is true
public async Task<IActionResult> ProcessOrder(Order order)
{
try
{
await _orderService.ProcessAsync(order);
return Ok();
}
catch (InvalidOperationException ex) when (ex.Message.Contains("inventory"))
{
// Handle inventory-specific errors
_logger.LogWarning(ex, "Inventory issue processing order");
return BadRequest("Item out of stock");
}
catch (InvalidOperationException ex) when (ex.Message.Contains("payment"))
{
// Handle payment-specific errors
_logger.LogError(ex, "Payment processing failed");
return BadRequest("Payment declined");
}
catch (InvalidOperationException ex)
{
// Handle other InvalidOperationException
_logger.LogError(ex, "Order processing failed");
return StatusCode(500, "Processing error");
}
}
// Filter with method
public async Task<User> GetUserAsync(int id)
{
try
{
return await _repository.GetByIdAsync(id);
}
catch (Exception ex) when (ShouldRetry(ex))
{
// Retry logic
await Task.Delay(1000);
return await _repository.GetByIdAsync(id);
}
}
private bool ShouldRetry(Exception ex)
{
return ex is TimeoutException ||
ex is HttpRequestException;
}
Global Error Handling in ASP.NET Core
// Exception handler middleware
public class GlobalExceptionHandler : IMiddleware
{
private readonly ILogger<GlobalExceptionHandler> _logger;
public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
{
_logger = logger;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
try
{
await next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception occurred");
await HandleExceptionAsync(context, ex);
}
}
private static async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
var (statusCode, message) = exception switch
{
ArgumentException => (400, exception.Message),
KeyNotFoundException => (404, "Resource not found"),
UnauthorizedAccessException => (401, "Unauthorized"),
InsufficientFundsException => (400, exception.Message),
_ => (500, "Internal server error")
};
context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/json";
await context.Response.WriteAsJsonAsync(new
{
error = message,
statusCode
});
}
}
// Register in Program.cs
builder.Services.AddTransient<GlobalExceptionHandler>();
var app = builder.Build();
app.UseMiddleware<GlobalExceptionHandler>();
Bad vs Good Examples
Bad: Swallowing Exceptions
// Bad - hides errors, makes debugging impossible
public void ProcessData()
{
try
{
// Complex operation
var result = PerformCalculation();
}
catch (Exception)
{
// Silently swallow - BAD!
}
}
// Bad - losing stack trace
try
{
DoWork();
}
catch (Exception ex)
{
throw ex; // BAD! Resets stack trace
}
Good: Proper Exception Handling
// Good - log and re-throw
public void ProcessData()
{
try
{
var result = PerformCalculation();
}
catch (Exception ex)
{
_logger.LogError(ex, "Calculation failed");
throw; // Preserves stack trace
}
}
// Good - wrap with more context
try
{
DoWork();
}
catch (Exception ex)
{
throw new ApplicationException("Work failed during processing", ex);
}
Bad: Catching Too Broadly
// Bad - catches everything including serious errors
try
{
ProcessOrder(order);
}
catch (Exception)
{
// Catches OutOfMemoryException, StackOverflowException, etc.
return "Order failed";
}
Good: Catch Specific Exceptions
// Good - only catch what you can handle
try
{
ProcessOrder(order);
}
catch (InvalidOperationException ex)
{
_logger.LogWarning(ex, "Invalid order state");
return "Invalid order";
}
catch (ArgumentException ex)
{
_logger.LogWarning(ex, "Invalid order data");
return "Invalid data";
}
// Let critical exceptions (OutOfMemory, etc.) propagate
Bad: Using Exceptions for Flow Control
// Bad - exceptions are expensive
public bool UserExists(int id)
{
try
{
var user = _repository.GetById(id);
return true;
}
catch (KeyNotFoundException)
{
return false; // Using exception for flow control
}
}
Good: Use Return Values for Flow Control
// Good - fast, clear intent
public bool UserExists(int id)
{
return _repository.TryGetById(id, out _);
}
// Or
public async Task<User?> TryGetUserAsync(int id)
{
var user = await _repository.FindByIdAsync(id);
return user; // Returns null if not found
}
Interview Tips
Tip 1: Always explain the difference between
throwandthrow ex- the former preserves stack trace, the latter resets it.
Tip 2: Mention that exceptions are expensive - use them for exceptional cases, not normal flow control.
Tip 3: Know when to catch vs when to let exceptions propagate. Catch only if you can meaningfully handle the error.
Tip 4: In ASP.NET Core, discuss both exception middleware and exception filters for global error handling.
Common Interview Questions
-
What's the difference between throw and throw ex?
throwpreserves the original stack trace.throw exresets the stack trace to the current location, losing valuable debugging information.
-
When should you create custom exceptions?
- When you need domain-specific error information, when built-in exceptions don't clearly express the error, or when you need to include additional data in the exception.
-
What is the purpose of finally block?
- Code in finally always executes whether exception occurs or not. Used for cleanup (closing files, connections). Modern C# often uses
usingstatements instead.
- Code in finally always executes whether exception occurs or not. Used for cleanup (closing files, connections). Modern C# often uses
-
What are exception filters?
- The
whenkeyword allows conditional catch blocks. Exception is only caught if the condition is true. Useful for handling same exception type differently based on state.
- The
-
How do you implement global error handling in ASP.NET Core?
- Use exception handling middleware (app.UseExceptionHandler), custom middleware, or exception filters. For APIs, return consistent error response format with status codes.
-
Should you catch Exception (base class)?
- Generally no - catch specific exceptions you can handle. Catching Exception can hide bugs and catch critical exceptions (OutOfMemoryException) that shouldn't be handled.
-
How expensive are exceptions in .NET?
- Very expensive - stack trace capture, unwinding, etc. Can be 1000x slower than normal return. Don't use for flow control or validation.