iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…