Exceptions
C# exceptions are objects thrown via throw, caught via try/catch. Use specific exception types, using for resource cleanup, and modern patterns: pattern matching, exception filters, expression-bodied throws.
Try, catch, custom, when filters, using
EXAMPLE
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
// 1) Basic try / catch
try
{
var text = File.ReadAllText("config.json");
Console.WriteLine(text);
}
catch (FileNotFoundException e)
{
Console.WriteLine("missing: " + e.FileName);
}
catch (IOException e)
{
Console.WriteLine("io: " + e.Message);
}
catch (Exception e)
{
Console.WriteLine("unexpected: " + e);
}
// 2) Finally
try { Work(); }
catch (Exception e) { Log(e); }
finally { Cleanup(); }
// 3) using — auto-Dispose for IDisposable
using (var stream = File.OpenRead("file.txt"))
using (var reader = new StreamReader(stream))
{
var content = reader.ReadToEnd();
}
// Disposed in reverse order, even on exception
// using declaration (C# 8+) — disposed at end of enclosing scope
using var stream = File.OpenRead("file.txt");
using var reader = new StreamReader(stream);
var content = reader.ReadToEnd();
// 4) Throw
throw new ArgumentException("age must be >= 0", nameof(age));
throw new InvalidOperationException("transaction not started");
throw new OrderNotFoundException(orderId);
// 5) Throw expression (C# 7+)
var name = input ?? throw new ArgumentNullException(nameof(input));
public string ProcessName(string name) =>
string.IsNullOrEmpty(name)
? throw new ArgumentException("name required", nameof(name))
: name.Trim();
// 6) Re-throw — preserve stack
catch (Exception e)
{
Log(e);
throw; // GOOD — preserves stack
// throw e; // BAD — resets stack to here
}
// Wrap with cause
catch (HttpRequestException e)
{
throw new ServiceException("API call failed", e); // InnerException preserved
}
// Inspect cause later
catch (ServiceException e)
{
var cause = e.InnerException;
}
// 7) Exception filters (C# 6+)
try
{
DoWork();
}
catch (HttpRequestException e) when (e.StatusCode == HttpStatusCode.NotFound)
{
// Specific handling for 404
}
catch (HttpRequestException e) when (e.StatusCode >= HttpStatusCode.InternalServerError)
{
// Server errors — retry
Retry();
}
catch (HttpRequestException e)
{
// All other HTTP errors
}
// Filters don't unwind the stack if false — useful for conditional logging
catch (Exception e) when (LogAndReturnFalse(e))
{
// Never reached; LogAndReturnFalse returns false
}
bool LogAndReturnFalse(Exception e)
{
log.Error(e);
return false;
}
// 8) Custom exception
public class OrderNotFoundException : Exception
{
public string OrderId { get; }
public OrderNotFoundException(string orderId)
: base($"Order not found: {orderId}")
{
OrderId = orderId;
}
public OrderNotFoundException(string orderId, Exception inner)
: base($"Order not found: {orderId}", inner)
{
OrderId = orderId;
}
}
// 9) Domain exception hierarchy
public abstract class DomainException : Exception
{
public string Code { get; }
protected DomainException(string code, string message, Exception? inner = null)
: base(message, inner) { Code = code; }
}
public sealed class OrderNotFound : DomainException { public OrderNotFound(string id) : base("ORDER_NOT_FOUND", $"Order {id} not found") { } }
public sealed class PaymentFailed : DomainException { public PaymentFailed(string r) : base("PAYMENT_FAILED", $"Payment failed: {r}") { } }
public sealed class InsufficientStock : DomainException { public InsufficientStock(string s, int q) : base("NO_STOCK", $"SKU {s} out of stock ({q} requested)") { } }
// 10) Async exceptions
public async Task<string> FetchAsync(string url)
{
try
{
using var client = new HttpClient();
var response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
catch (HttpRequestException e)
{
log.Error("http failed: " + e.Message);
throw;
}
catch (TaskCanceledException)
{
log.Warn("timeout");
throw new TimeoutException("request timed out");
}
}
// 11) AggregateException — multiple inner exceptions (parallel work)
try
{
await Task.WhenAll(tasks);
}
catch (Exception e)
{
// WhenAll throws the FIRST exception
// To see all, use ContinueWith with status or check Task.IsFaulted
}
Task.WaitAll(tasks); // Throws AggregateException with all inner exceptions
// 12) When exceptions are wrong
// Don't use for control flow
int? ParseInt(string s)
{
try { return int.Parse(s); }
catch (FormatException) { return null; }
}
// Better: TryParse
if (int.TryParse(s, out var n)) { /* use n */ }
else { /* invalid */ }
// 13) Pattern matching catches (C# 7+)
try { /* ... */ }
catch (Exception e) when (e is FileNotFoundException or DirectoryNotFoundException)
{
// Either type
}
// switch on exception
var message = e switch
{
OrderNotFound onf => $"Unknown order {onf.OrderId}",
PaymentFailed pf => $"Payment: {pf.Message}",
InsufficientStock i => $"Out of stock",
_ => "Internal error",
};
// 14) Null checks
ArgumentNullException.ThrowIfNull(input); // .NET 6+
ArgumentException.ThrowIfNullOrEmpty(input); // .NET 7+
ArgumentOutOfRangeException.ThrowIfNegative(value, nameof(value)); // .NET 8+
ArgumentOutOfRangeException.ThrowIfGreaterThan(value, max, nameof(value));
// Old
if (input is null) throw new ArgumentNullException(nameof(input));
// 15) Performance
// • Throwing is expensive (~5-50μs) — don't use for normal flow
// • Catching is fast (~ns)
// • Stack trace capture dominates cost
// • Use TryParse / TryGetValue / Result pattern for hot paths
// 16) Async exception handling pitfalls
// Lost exception — fire-and-forget
DoSomethingAsync(); // exception swallowed
_ = DoSomethingAsync(); // explicit; same problem
// Better — log explicitly
_ = Task.Run(async () =>
{
try { await DoSomethingAsync(); }
catch (Exception e) { log.Error("task failed", e); }
});
// async void — exceptions can't be awaited
async void OnClick(object s, EventArgs e)
{
try { await DoSomethingAsync(); }
catch (Exception ex) { log.Error(ex); }
}
// 17) Exception filters for retries
int attempts = 0;
while (true)
{
try
{
await DoAsync();
break;
}
catch (HttpRequestException) when (++attempts < 3)
{
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempts)));
}
}
// Better: Polly library for production retry policies
// 18) Global error handling (ASP.NET Core)
app.UseExceptionHandler(handler =>
handler.Run(async context =>
{
var feature = context.Features.Get<IExceptionHandlerFeature>();
var e = feature?.Error;
log.Error(e, "unhandled");
context.Response.StatusCode = 500;
await context.Response.WriteAsJsonAsync(new {
error = "internal",
requestId = context.TraceIdentifier,
});
}));
// Or via middleware / ProblemDetails:
builder.Services.AddProblemDetails();
// 19) Common bugs
// ❌ Empty catch — silently swallows errors
// ❌ Catching Exception (or worse, Object) blindly
// ❌ `throw e;` instead of `throw;` (resets stack)
// ❌ Using exceptions for control flow (TryParse exists)
// ❌ Forgetting to dispose IDisposable (use using)
// ❌ async void instead of async Task
// ❌ Re-throwing the wrong exception (lose context)
// 20) Best practices
// ✅ Use specific exception types (FileNotFoundException, not Exception)
// ✅ Domain-specific custom exceptions for predictable errors
// ✅ Always include InnerException when wrapping
// ✅ Use exception filters (when clause) for conditional handling
// ✅ Pattern match on exceptions in switch expressions
// ✅ Argument validation at the boundary with ThrowIfNull / ThrowIfNullOrEmpty
// ✅ Use using for IDisposable; ConfigureAwait(false) in libraries
// ✅ Polly for retries / circuit breaker in production
// ✅ Global error handler in ASP.NET Core for consistent responses
Why it matters
throw; not throw e;. Use specific exception types, exception filters (when) for conditional handling, and ArgumentNullException.ThrowIfNull for clean boundary checks. using handles cleanup.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
try {
int n = int.Parse(input);
} catch (FormatException e) {
Console.Error.WriteLine(\$"bad input: {e.Message}");
} finally {
Cleanup();
}
Try it Yourself »
Discussion
Loading…