Chuyển tới nội dung chính

Exception Handling

Definition

Exception handling is a mechanism to detect, respond to, and recover from runtime errors gracefully. C# uses a structured approach with try, catch, finally, and throw blocks built on the .NET exception hierarchy rooted at System.Exception.

Core Concepts

try / catch / finally Blocks

try
{
var result = ParseInput(input);
SaveToDatabase(result);
}
catch (FormatException ex)
{
Console.WriteLine($"Invalid format: {ex.Message}");
}
catch (DbUpdateException ex)
{
Console.WriteLine($"Database error: {ex.Message}");
}
finally
{
CloseConnection(); // Always executes
}

Catching Specific Exceptions

Catch blocks are evaluated in order. Always catch the most specific exception type first:

try
{
File.ReadAllText(path);
}
catch (FileNotFoundException ex)
{
// Most specific first
HandleMissingFile(path);
}
catch (UnauthorizedAccessException ex)
{
HandlePermissionError(ex);
}
catch (IOException ex)
{
// More general — catches other IO errors
HandleIoError(ex);
}
// Do NOT add catch (Exception) here unless you truly need it

throw and Re-throw

throw vs throw ex

Use throw; to re-throw an exception while preserving the original stack trace. Using throw ex; resets the stack trace, making debugging much harder.

catch (SqlException ex)
{
_logger.LogError(ex, "Database operation failed");
throw; // Preserves stack trace
}

// Anti-pattern — NEVER do this:
catch (SqlException ex)
{
_logger.LogError(ex, "Database operation failed");
throw ex; // Destroys stack trace!
}

finally Block

The finally block always executes regardless of whether an exception was thrown or caught. Use it for cleanup:

FileStream? stream = null;
try
{
stream = File.OpenRead(path);
ProcessStream(stream);
}
finally
{
stream?.Dispose(); // Guaranteed cleanup
}
Prefer using declarations

In modern C#, using declarations handle disposal automatically and are preferred over try/finally for resource cleanup:

using var stream = File.OpenRead(path);
ProcessStream(stream); // Dispose called automatically at scope end

Common Exception Types

Exception TypeWhen It Occurs
NullReferenceExceptionDereferencing a null reference
ArgumentNullExceptionPassing null to a non-nullable parameter
ArgumentExceptionInvalid argument value
ArgumentOutOfRangeExceptionArgument outside allowed range
InvalidOperationExceptionObject state does not allow the operation
FormatExceptionInvalid string format for parsing
IndexOutOfRangeExceptionArray/indexer out of bounds
KeyNotFoundExceptionKey missing from dictionary
NotImplementedExceptionMethod not yet implemented
ObjectDisposedExceptionOperating on a disposed object
TimeoutExceptionOperation exceeded time limit
AggregateExceptionCombines multiple exceptions (common in TPL)

Custom Exceptions

Create custom exceptions when you need domain-specific error types that callers can handle distinctly:

[Serializable]
public class PaymentFailedException : Exception
{
public decimal Amount { get; }
public string TransactionId { get; }

public PaymentFailedException() { }

public PaymentFailedException(string message)
: base(message) { }

public PaymentFailedException(string message, Exception innerException)
: base(message, innerException) { }

public PaymentFailedException(string message, decimal amount, string transactionId)
: base(message)
{
Amount = amount;
TransactionId = transactionId;
}

// Serialization constructor — required for proper serialization support
protected PaymentFailedException(
SerializationInfo info, StreamingContext context)
: base(info, context)
{
Amount = info.GetDecimal(nameof(Amount));
TransactionId = info.GetString(nameof(TransactionId))!;
}
}

Exception Filters (C# 6)

Use catch...when to filter exceptions before entering the catch block:

try
{
ExecuteHttpCall(request);
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests)
{
await Task.Delay(TimeSpan.FromSeconds(5));
retry = true;
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
return Result.NotFound();
}
catch (HttpRequestException ex)
{
// All other HTTP errors
return Result.Failure(ex.Message);
}

Exception filters do not reset the stack trace, making them ideal for conditional logging:

catch (Exception ex) when (_logger.LogAndReturnFalse(ex))
{
// Never entered — LogAndReturnFalse returns false
// But the exception was logged with full stack trace
}

Inner Exceptions

Chain exceptions to preserve context while wrapping lower-level errors:

try
{
_httpClient.PostAsync(url, content).Wait();
}
catch (AggregateException ex)
{
throw new PaymentGatewayException(
"Failed to contact payment provider", ex.InnerException!);
}

Guard Clauses

Use guard clauses to fail fast at the top of a method:

public void ProcessOrder(Order? order)
{
ArgumentNullException.ThrowIfNull(order); // .NET 6+
ArgumentException.ThrowIfNullOrEmpty(order.Id); // .NET 8+

if (order.Items.Count == 0)
throw new ArgumentException("Order must contain items.", nameof(order));

if (order.Total <= 0)
throw new ArgumentOutOfRangeException(nameof(order), "Total must be positive.");

// Main logic starts here — all preconditions validated
FulfillOrder(order);
}

When to Use

  • I/O operations — file, network, and database calls can always fail
  • Parsing external input — user input, configuration files, API payloads
  • Resource cleanup — always pair acquisition with finally or using
  • Domain validation — throw custom exceptions for business rule violations
  • Boundary layers — catch and translate exceptions at service/API boundaries

Common Pitfalls

Catching Exception base type

Avoid catch (Exception) unless you are at a top-level handler (e.g., middleware). Swallowing all exceptions hides bugs and makes debugging extremely difficult. If you must catch Exception, always re-throw or log with full context.

async void exception crashes

Exceptions thrown in async void methods propagate to the synchronization context and will crash the process. Always use async Task instead of async void, except for event handlers.

finally that throws

If a finally block throws an exception, it replaces the original exception. Keep finally blocks simple — only dispose resources.

Prefer TryParse over Parse+catch

For converting strings to numbers, use int.TryParse, decimal.TryParse, etc. The exception-based Parse approach is significantly slower when failures are expected:

// Good — no exception on invalid input
if (int.TryParse(input, out var number))
ProcessNumber(number);

// Bad — exception for expected control flow
try { ProcessNumber(int.Parse(input)); }
catch (FormatException) { HandleInvalidInput(); }

Key Takeaways

  1. Always catch the most specific exception type first.
  2. Use throw; (not throw ex;) to re-throw and preserve the stack trace.
  3. Use finally or using for guaranteed resource cleanup.
  4. Create custom exceptions for domain-specific errors that callers need to handle distinctly.
  5. Exception filters (catch...when) are powerful for conditional handling without losing stack traces.
  6. Guard clauses at method entry points keep the main logic clean.
  7. Never swallow exceptions — at minimum, log them.

Interview Questions

Q: What is the difference between throw and throw ex? throw; re-throws the caught exception while preserving the original stack trace. throw ex; resets the stack trace to the current method, hiding where the exception originally occurred. Always use throw; when re-throwing.

Q: What is the difference between finally and catch? catch handles a specific exception type and only runs when that exception occurs. finally always runs regardless of whether an exception was thrown or caught. Use catch for error handling and finally for cleanup.

Q: When should you create custom exceptions? Create custom exceptions when callers need to handle a specific domain error differently from general errors. Name them descriptively (e.g., InsufficientFundsException), inherit from Exception, and include a serialization constructor.

Q: What is an exception filter? An exception filter (catch...when) is a C# 6 feature that evaluates a condition before entering a catch block. If the condition is false, the catch block is skipped and the exception continues propagating. Filters preserve the stack trace and avoid re-throwing.

Q: Why should you avoid async void? Exceptions thrown in async void methods cannot be caught by the caller and will crash the application via the synchronization context. Use async Task so the caller can await and handle exceptions.

References