Obsługa błędów w C#

Dobra obsługa błędów decyduje o tym, czy aplikacja jest niezawodna i diagnozowalna. W C# służą do tego głównie wyjątki oraz dobre praktyki walidacji i logowania. Poniżej zestaw wzorców używanych w realnych projektach.

1. try-catch-finally

try
{
    // kod, który może zgłosić wyjątek
}
catch (Exception ex)
{
    Console.WriteLine($"Wystąpił błąd: {ex.Message}");
}
finally
{
    // wykonuje się zawsze — niezależnie od tego, czy był wyjątek
}

2. Zarządzanie zasobami: using

Blok using gwarantuje zwolnienie zasobu (plik, połączenie) także w razie wyjątku.

try
{
    using var plik = new FileStream("plik.txt", FileMode.Open);
    // praca z plikiem
}
catch (IOException ex)
{
    Console.WriteLine($"Błąd IO: {ex.Message}");
}

3. Walidacja danych i guard clauses

Wiele błędów lepiej uprzedzić niż później przechwytywać. W metodach publicznych sprawdzaj argumenty na wejściu. We współczesnym .NET (8+) służą do tego zwięzłe helpery, np. ArgumentNullException.ThrowIfNull i ArgumentOutOfRangeException.ThrowIfNegativeOrZero.

public void Przetwarzaj(string nazwa, int liczba)
{
    ArgumentNullException.ThrowIfNull(nazwa);
    ArgumentOutOfRangeException.ThrowIfNegativeOrZero(liczba);

    // dalszy kod ma już pewność, że dane są poprawne
}

ℹ️ Uwaga: nie używaj do tego Code Contracts (Contract.Requires z System.Diagnostics.Contracts). To porzucona technologia, która nie działa we współczesnym .NET. Stosuj guard clauses jak wyżej.

4. Rzucanie wyjątków

Gdy metoda nie może kontynuować, zgłoś wyjątek najlepiej dopasowanego typu wbudowanego (np. ArgumentException, InvalidOperationException).

public void Ustaw(int x)
{
    if (x <= 0)
    {
        throw new ArgumentException("x musi być większe od zera.", nameof(x));
    }
}

ℹ️ Nie rzucaj i nie dziedzicz po ApplicationException — to typ historyczny, dziś odradzany. Użyj konkretnego wyjątku wbudowanego albo własnej klasy (poniżej).

5. Własne wyjątki (custom exceptions)

Tworząc własny wyjątek, zadbaj o konstruktory — w tym ten przyjmujący wyjątek wewnętrzny (innerException), potrzebny przy opakowywaniu błędów.

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

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

6. Wiele bloków catch

Obsługuj różne typy wyjątków osobno — od najbardziej szczegółowych do najbardziej ogólnych.

try
{
    // kod
}
catch (CustomException ex)
{
    Console.WriteLine($"Błąd niestandardowy: {ex.Message}");
}
catch (DivideByZeroException ex)
{
    Console.WriteLine($"Dzielenie przez zero: {ex.Message}");
}
catch (Exception ex)
{
    Console.WriteLine($"Inny błąd: {ex.Message}");
}

7. Filtry wyjątków: when

Słowo kluczowe when pozwala obsłużyć catch tylko, gdy spełniony jest dodatkowy warunek (bez „połykania” innych przypadków).

try
{
    // kod
}
catch (IOException ex) when (ex.Message.Contains("plik"))
{
    Console.WriteLine($"Błąd IO związany z plikiem: {ex.Message}");
}
catch (IOException ex)
{
    Console.WriteLine($"Inny błąd IO: {ex.Message}");
}

8. Ponowne zgłaszanie: throw vs throw ex

W bloku catch używaj samego throw;, aby zachować oryginalny ślad stosu. throw ex; go kasuje. Możesz też opakować błąd we własny wyjątek, podając oryginalny jako innerException.

try
{
    // kod
}
catch (IOException ex)
{
    Console.WriteLine($"Błąd IO: {ex.Message}");
    throw;   // ponowne zgłoszenie z zachowaniem stack trace
}
catch (Exception ex)
{
    // opakowanie w wyjątek domenowy
    throw new CustomException("Operacja nie powiodła się.", ex);
}

9. Wyjątki w kodzie asynchronicznym

Najważniejsza zasada: przy await wyjątek jest rozpakowany — łapiesz oryginalny typ bezpośrednio, a nie AggregateException.

try
{
    await OperacjaAsync();
}
catch (InvalidOperationException ex)   // łapiemy konkretny typ
{
    Console.WriteLine($"Błąd: {ex.Message}");
}

AggregateException pojawia się w innych sytuacjach: przy blokującym dostępie do wyniku (.Result, .Wait()) oraz przy Task.WhenAll, gdzie może wystąpić wiele błędów naraz.

try
{
    await Task.WhenAll(zadanie1, zadanie2, zadanie3);
}
catch (Exception)
{
    // await rzuca PIERWSZY wyjątek; pełną listę znajdziesz w zadaniach:
    var wszystkie = new[] { zadanie1, zadanie2, zadanie3 }
        .Where(t => t.IsFaulted)
        .SelectMany(t => t.Exception!.InnerExceptions);

    foreach (var e in wszystkie)
    {
        Console.WriteLine(e.Message);
    }
}

10. Logowanie błędów

W aplikacjach produkcyjnych zamiast Console.WriteLine używaj loggera (np. ILogger w ASP.NET Core) i loguj z kontekstem.

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

    public Serwis(ILogger<Serwis> logger) => _logger = logger;

    public void Wykonaj(int id)
    {
        try
        {
            // kod
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Błąd podczas przetwarzania rekordu {Id}", id);
            throw;
        }
    }
}

11. Globalna obsługa błędów w ASP.NET Core

W aplikacjach webowych skonfiguruj jedno, centralne miejsce obsługi nieobsłużonych wyjątków. W nowszych projektach robisz to w Program.cs:

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

Middleware UseExceptionHandler przekierowuje użytkownika na wskazaną stronę w razie błędu. Dla API zwykle zwraca się ustandaryzowaną odpowiedź (np. Problem Details).

12. Testowanie obsługi błędów

Sprawdzaj w testach, czy metoda zgłasza oczekiwany wyjątek. W xUnit służy do tego Assert.Throws:

[Fact]
public void Dzielenie_PrzezZero_RzucaWyjatek()
{
    Assert.Throws<DivideByZeroException>(() => Kalkulator.Dzielenie(42, 0));
}

13. Sytuacje nieodwracalne: Environment.FailFast

Gdy stan aplikacji jest na tyle uszkodzony, że kontynuacja jest niebezpieczna, można natychmiast zakończyć proces. To rozwiązanie ostateczne — stosuj wyjątkowo.

Environment.FailFast("Nieodwracalny błąd — zamykam aplikację.");

Podsumowanie

  • Uprzedzaj błędy walidacją wejścia (guard clauses / ThrowIf...), zamiast tylko je przechwytywać.
  • Łap konkretne typy wyjątków; catch (Exception) zostaw jako ostatnią deskę ratunku.
  • Przy await łapiesz oryginalny wyjątek; AggregateException to świat .Result/.Wait()/WhenAll.
  • Zachowuj ślad stosu przez throw; i opakowuj błędy z innerException.
  • Loguj z kontekstem i centralizuj obsługę błędów w aplikacjach webowych.
  • Nie używaj Code Contracts ani ApplicationException — to przestarzałe podejścia.

6 comments

  1. Bardzo przydatne informacje! Chciałbym tylko dodać, że zawsze warto być precyzyjnym w obsłudze błędów, aby uniknąć zgłaszania ogólnych wyjątków. To pozwala na lepsze zrozumienie problemu i szybszą reakcję na potencjalne problemy.

  2. Dobre przypomnienie o niwelowaniu błędów poprzez sprawdzanie działania (Code Contracts). Warto stosować asercje i pre/post-warunki, zwłaszcza w większych projektach, aby zminimalizować ryzyko błędów.

  3. Doświadczonego Programisty pisze:

    Dostosowane wyjątki są kluczowe w zrozumieniu kontekstu błędu. Dzięki nim łatwiej jest diagnozować i naprawiać problemy. Polecam również przemyślenie, czy warto tworzyć hierarchię dostosowanych wyjątków dla lepszej organizacji.

  4. Entuzjasty Asynchroniczności pisze:

    Dobre przypomnienie o obsłudze błędów w operacjach asynchronicznych. W świecie async/await błędy mogą być zgłaszane w różny sposób, więc ważne jest, aby skoncentrować się na poprawnej obsłudze tych przypadków.

  5. Fanatyka Bezpieczeństwa pisze:

    Obsługa błędów to nie tylko kwestia niezawodności, ale także bezpieczeństwa. Warto pamiętać o zabezpieczaniu krytycznych informacji przed wyciekiem podczas obsługi błędów.

  6. Korzystam z obsługi błędów w delegatach i wyrażeniach lambda, ale zawsze zastanawiałem się, czy istnieją jakieś subtelności, których powinienem unikać. Dzięki za podzielenie się praktykami w tym obszarze!

Dodaj komentarz