Real‑world examples of C# exception handling: 3 practical patterns every developer should know

If you write C# and touch databases, APIs, or file systems, you already know things break in production long before you’re ready. That’s where good exception handling earns its paycheck. In this guide, we’ll walk through real‑world examples of C# exception handling: 3 practical examples that you can drop straight into your own codebase. Instead of abstract theory, we’ll look at how to wrap file I/O, HTTP calls, and validation logic so that failures are predictable, logged, and testable. These examples of C# exception handling: 3 practical examples are not just about `try`/`catch` syntax. We’ll talk about when to throw versus return error codes, how to avoid swallowing exceptions, and how to design custom exceptions that make debugging faster for your future self (or the unlucky person inheriting your code). Along the way, we’ll layer in extra scenarios—retry logic, async code, and defensive validation—that mirror what professional .NET teams are actually shipping in 2024 and 2025.
Written by
Jamie
Published

Example‑first tour of C# exception handling

Most articles start with theory. Let’s skip that and go straight to code. We’ll build three core patterns that cover the best examples of how C# exception handling actually shows up in production:

  • File and configuration errors
  • Network and API errors
  • Business‑rule and validation errors

Around those three pillars, we’ll add more examples that show retries, async handling, and custom exception types. By the end, you’ll have multiple examples of C# exception handling: 3 practical examples at the center, plus several variations you can adapt.


Pattern 1: File and configuration failures (with logging)

File I/O is one of the most common real examples of C# exception handling, because it fails in all the boring ways: missing files, locked files, wrong permissions, corrupted config.

Here’s a simple pattern you can reuse for reading JSON configuration safely:

using System;
using System.IO;
using System.Text.Json;
using Microsoft.Extensions.Logging;

public class AppConfig
{
    public string ConnectionString { get; set; } = string.Empty;
    public int MaxRetries { get; set; } = 3;
}

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

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

    public AppConfig Load(string path)
    {
        try
        {
            if (!File.Exists(path))
            {
                throw new FileNotFoundException($"Config file not found: {path}", path);
            }

            var json = File.ReadAllText(path);
            var config = JsonSerializer.Deserialize<AppConfig>(json);

            if (config == null)
            {
                throw new InvalidDataException("Config file content could not be deserialized.");
            }

            return config;
        }
        catch (FileNotFoundException ex)
        {
            _logger.LogError(ex, "Missing configuration file at {Path}", path);
            throw; // bubble up, this is not recoverable here
        }
        catch (JsonException ex)
        {
            _logger.LogError(ex, "Invalid JSON in configuration file at {Path}", path);
            throw new InvalidDataException("Configuration file is not valid JSON.", ex);
        }
        catch (IOException ex)
        {
            _logger.LogWarning(ex, "I/O error while reading configuration file at {Path}", path);
            throw; // caller decides what to do
        }
    }
}
``

This single snippet already hides several examples of C# exception handling:

- Guarding against missing files with `FileNotFoundException`.
- Translating low‑level `JsonException` into a higher‑level `InvalidDataException` that callers understand.
- Logging with context, then rethrowing so the error is not silently ignored.

### Variation: Safe default configuration instead of failure

Sometimes you don’t want the app to crash just because a config file is missing in development. Here’s another example of C# exception handling where you fail gracefully and provide defaults:

```csharp
public AppConfig TryLoadOrDefault(string path)
{
    try
    {
        return Load(path);
    }
    catch (FileNotFoundException)
    {
        _logger.LogInformation("Config file missing; using default configuration.");
        return new AppConfig
        {
            ConnectionString = "Server=localhost;Database=Test;Trusted_Connection=True;",
            MaxRetries = 1
        };
    }
}

This illustrates that good examples include not just catching exceptions, but deciding when it’s acceptable to continue with a safe fallback.


Pattern 2: HTTP and API failures with retries and timeouts

If you consume APIs, you’re going to see timeouts, DNS failures, and 500 responses. These are perfect examples of C# exception handling: 3 practical examples often center on network calls because they fail in ways you can’t control.

Here’s a realistic pattern using HttpClient with retry handling and exception translation:

using System;
using System.Net.Http;
using System.Net;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

public class WeatherClient
{
    private readonly HttpClient _httpClient;

    public WeatherClient(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<WeatherForecast> GetForecastAsync(string city, CancellationToken ct = default)
    {
        const int maxAttempts = 3;
        var attempt = 0;

        while (true)
        {
            attempt++;

            try
            {
                using var response = await _httpClient.GetAsync($"/api/forecast?city={city}", ct);

                if (response.StatusCode == HttpStatusCode.NotFound)
                {
                    throw new CityNotFoundException(city);
                }

                response.EnsureSuccessStatusCode();

                var json = await response.Content.ReadAsStringAsync(ct);
                var result = JsonSerializer.Deserialize<WeatherForecast>(json);

                return result ?? throw new InvalidDataException("Empty weather response.");
            }
            catch (CityNotFoundException)
            {
                // Known business case: do not retry
                throw;
            }
            catch (HttpRequestException ex) when (attempt < maxAttempts)
            {
                // Transient network error: retry
                await Task.Delay(TimeSpan.FromMilliseconds(200 * attempt), ct);
                continue;
            }
            catch (TaskCanceledException ex) when (!ct.IsCancellationRequested)
            {
                // Treat as timeout
                throw new TimeoutException("Weather service request timed out.", ex);
            }
        }
    }
}

public class CityNotFoundException : Exception
{
    public string City { get; }

    public CityNotFoundException(string city)
        : base($"City not found: {city}")
    {
        City = city;
    }
}

Why this belongs among the best examples of C# exception handling:

  • Uses when filters to retry only transient HttpRequestException cases.
  • Introduces a domain‑specific CityNotFoundException instead of leaking raw HTTP status codes.
  • Translates TaskCanceledException into a clearer TimeoutException.

Variation: Wrapping external APIs for a clean boundary

In larger systems, you often wrap third‑party APIs behind your own interface. That wrapper is a great place to concentrate your exception handling. For example:

public interface IPaymentGateway
{
    Task ProcessPaymentAsync(PaymentRequest request, CancellationToken ct = default);
}

public class StripePaymentGateway : IPaymentGateway
{
    public async Task ProcessPaymentAsync(PaymentRequest request, CancellationToken ct = default)
    {
        try
        {
            // Call Stripe SDK or REST API here
        }
        catch (HttpRequestException ex)
        {
            throw new PaymentUnavailableException("Payment service unavailable.", ex);
        }
        catch (TaskCanceledException ex)
        {
            throw new PaymentTimeoutException("Payment request timed out.", ex);
        }
    }
}

This is another example of C# exception handling where you shield the rest of your code from low‑level transport details.


Pattern 3: Validation and business‑rule exceptions

So far we’ve looked at I/O and network issues. The third category in our set of examples of C# exception handling: 3 practical examples is business‑rule and validation logic—where you decide that input is not acceptable and you want to fail loudly.

Consider an order processing service:

public class Order
{
    public Guid Id { get; set; }
    public decimal Amount { get; set; }
    public string Currency { get; set; } = "USD";
}

public class InvalidOrderException : Exception
{
    public InvalidOrderException(string message) : base(message) { }
}

public class OrderService
{
    public void Process(Order order)
    {
        if (order == null)
        {
            throw new ArgumentNullException(nameof(order));
        }

        if (order.Amount <= 0)
        {
            throw new InvalidOrderException("Order amount must be greater than zero.");
        }

        if (order.Currency != "USD" && order.Currency != "EUR")
        {
            throw new InvalidOrderException($"Unsupported currency: {order.Currency}");
        }

        try
        {
            // Persist to database, call payment service, etc.
        }
        catch (SqlException ex)
        {
            // In real projects, use Microsoft.Data.SqlClient
            throw new OrderPersistenceException("Failed to save order.", ex);
        }
    }
}

public class OrderPersistenceException : Exception
{
    public OrderPersistenceException(string message, Exception inner)
        : base(message, inner) { }
}

Here, examples include:

  • Using ArgumentNullException for programming‑time errors.
  • Throwing InvalidOrderException for domain validation failures.
  • Wrapping SqlException in an OrderPersistenceException so callers don’t depend on a specific database provider.

Variation: Aggregating multiple validation errors

In APIs, you often want to return all validation problems in one shot. This is a more advanced example of C# exception handling where you aggregate errors:

public class ValidationException : Exception
{
    public IReadOnlyList<string> Errors { get; }

    public ValidationException(IEnumerable<string> errors)
        : base("Validation failed.")
    {
        Errors = errors.ToList();
    }
}

public static class OrderValidator
{
    public static void Validate(Order order)
    {
        var errors = new List<string>();

        if (order.Amount <= 0)
            errors.Add("Amount must be positive.");

        if (string.IsNullOrWhiteSpace(order.Currency))
            errors.Add("Currency is required.");

        if (errors.Count > 0)
            throw new ValidationException(errors);
    }
}

This gives API controllers a single ValidationException to catch and map to a 400 Bad Request response.


Modern 2024–2025 practices around C# exception handling

Let’s zoom out from these examples of C# exception handling: 3 practical examples and look at how modern .NET teams treat exceptions.

Prefer exceptions for exceptional conditions

With .NET 6/7/8 and C# 10/11/12, the guidance hasn’t changed: exceptions are for exceptional cases, not control flow. Repeatedly throwing exceptions in hot paths is still expensive.

Typical patterns in recent codebases:

  • Use return types like bool TryParse(...) or TryGetValue(...) for common, expected failures.
  • Reserve exceptions for bugs, data corruption, or situations where the caller truly can’t continue.

Microsoft’s official documentation on exceptions in .NET still reflects this approach.

Centralized exception handling in ASP.NET Core

In web apps, you rarely scatter try/catch in every controller. Instead, you configure a global exception handler middleware:

app.UseExceptionHandler("/error");

app.Map("/error", (HttpContext http) =>
{
    var ex = http.Features.Get<IExceptionHandlerFeature>()?.Error;

    // Map known exceptions to status codes
    return ex switch
    {
        ValidationException ve => Results.BadRequest(new { errors = ve.Errors }),
        CityNotFoundException => Results.NotFound(),
        _ => Results.Problem()
    };
});

This approach keeps your examples of C# exception handling mostly in the domain and infrastructure layers, while the web layer focuses on mapping them to HTTP responses.

Observability: logging and metrics

Unhandled exceptions in production should be rare, and you want visibility when they happen. Modern setups combine structured logging with monitoring tools. While health‑oriented sites like Mayo Clinic and NIH focus on patient safety rather than software, the same mindset applies: monitor what matters, respond quickly, and prevent repeat incidents.

In .NET, this often means:

  • Logging exceptions with contextual properties (user id, correlation id, request path).
  • Emitting metrics on exception rates per endpoint or service.

More real examples of C# exception handling in everyday code

To round things out, here are a few more short, realistic examples that build on the three main patterns:

Example: Async background job with defensive exception handling

public class EmailJob
{
    private readonly IEmailSender _sender;
    private readonly ILogger<EmailJob> _logger;

    public EmailJob(IEmailSender sender, ILogger<EmailJob> logger)
    {
        _sender = sender;
        _logger = logger;
    }

    public async Task RunAsync(CancellationToken ct)
    {
        try
        {
            await _sender.SendPendingEmailsAsync(ct);
        }
        catch (SmtpException ex)
        {
            _logger.LogError(ex, "SMTP failure while sending emails.");
            // Let the job scheduler decide whether to retry
            throw;
        }
        catch (Exception ex)
        {
            _logger.LogCritical(ex, "Unexpected error in EmailJob.");
            throw;
        }
    }
}

Example: Library method that never throws

Sometimes the best example of C# exception handling is intentionally not throwing. You can wrap exception‑throwing APIs in safe helpers:

public static class SafeFile
{
    public static bool TryReadAllText(string path, out string? content)
    {
        try
        {
            content = File.ReadAllText(path);
            return true;
        }
        catch (IOException)
        {
            content = null;
            return false;
        }
    }
}

Callers can then decide whether a missing file is a big deal without paying the cost of exceptions in the hot path.

These smaller snippets complement our earlier examples of C# exception handling: 3 practical examples centered on files, HTTP, and business rules, giving you a broader toolbox for everyday coding.


FAQ: common questions about C# exception handling

What are some real‑world examples of C# exception handling in web APIs?

In web APIs, common examples include:

  • Wrapping database exceptions (like SqlException) into domain‑specific errors.
  • Translating ValidationException into HTTP 400 responses.
  • Mapping UnauthorizedAccessException to HTTP 401/403.
  • Converting TimeoutException from external services into 504 Gateway Timeout.

All of these build on the same patterns shown in the earlier examples of C# exception handling: 3 practical examples that focus on I/O, HTTP, and validation.

When should I create a custom exception type in C#?

Create a custom exception when callers need to distinguish a specific failure from other errors in code. For example, CityNotFoundException or PaymentTimeoutException make it easy to catch and handle those cases separately. If no caller will ever treat the error differently, a standard exception type like InvalidOperationException or ArgumentException is usually enough.

Is it better to return error codes or throw exceptions in C#?

For library and framework code, the modern .NET guidance favors exceptions for unexpected failures and TryXxx patterns for expected ones. Returning error codes often leads to ignored failures. That said, in high‑performance or real‑time systems, you may prefer TryXxx methods and avoid throwing in hot loops. Your examples of C# exception handling should reflect the performance and reliability needs of your application.

How do I avoid swallowing exceptions by mistake?

Avoid empty catch blocks. If you catch an exception, either:

  • Log it and rethrow (using throw; to preserve the stack trace), or
  • Translate it into a more meaningful exception, or
  • Handle it fully with a clear reason (for example, using a safe default configuration in development).

If you find yourself writing catch (Exception) { }, stop and think about what you’re hiding.

Can you show a simple example of logging exceptions in C#?

A minimal example of logging exceptions with ILogger looks like this:

try
{
    await service.DoWorkAsync();
}
catch (Exception ex)
{
    _logger.LogError(ex, "Error while running background work.");
    throw;
}

This pattern shows up in many real examples of C# exception handling because it preserves the exception while giving you observability in production logs.


If you treat these examples of C# exception handling: 3 practical examples as starting points—file/config loading, HTTP/API calls, and validation/business rules—you’ll cover most of the failure modes that bite .NET applications in production. From there, you can refine your own patterns for your team’s performance, reliability, and debugging needs.

Explore More C++ Code Snippets

Discover more examples and insights in this category.

View All C++ Code Snippets