CodeGym /Kursy /C# SELF /Podsumowana strategia obsługi błędów:

Podsumowana strategia obsługi błędów: Task, async/ await, AggregateException

C# SELF
Poziom 61 , Lekcja 4
Dostępny

1. Wprowadzenie

Pisać programy, które nigdy nie padają – to tak, jakby wierzyć, że bugi boją się twojego IDE. W rzeczywistości im bardziej skomplikowany kod (zwłaszcza w środowisku wielowątkowym i asynchronicznym), tym bardziej pomysłowe są błędy i bardziej wyrafinowane wyjątki. Ignorując wyjątki w wątkach i zadaniach, łatwo dostać wycieki zasobów, zawieszenia, utratę danych lub niespodziewane awarie godzinę po uruchomieniu.

W tym wykładzie zbierzemy najlepsze praktyki obsługi błędów w programowaniu wielowątkowym i asynchronicznym w C#. Dowiesz się, jak poprawnie łapać wyjątki, jak rozbierać „kupę” jednoczesnych błędów (w stylu AggregateException), co robić z zadaniami uruchomionymi „na odwal” i dlaczego ignorowanie wyjątków — jest niebezpieczne i bezsensowne.

Dlaczego to nie jest proste

  • Wątek, w którym występuje błąd, może być inny niż wątek, gdzie umieszczono try-catch.
  • Asynchroniczne zadania nie wyrzucają wyjątków od razu — są „zapakowane” i czekają na obsłużenie przy await (lub przy synchronicznym oczekiwaniu).
  • Przy jednoczesnej pracy z wieloma zadaniami (np. Task.WhenAll) może wystąpić kilka błędów — trzeba je uwzględnić.
  • Operacje typu fire-and-forget mogą całkowicie „zgubić” wyjątek bez jawnego handlera.

Ta cecha to rozdzielenie kontekstu wykonania. Wyobraź sobie program jako cyrk z kilkoma arenami: jeśli na jednej wybuchnie pożar, nie od razu widać to na pozostałych. Ważne jest, by umieć prawidłowo śledzić i gasić takie „pożary”.

2. Wyjątki w zadaniach: Task i Task<TResult>

Jak zadania sygnalizują błędy

Kiedy w zadaniu wystąpi nieobsłużony wyjątek, on nie „wyleci” od razu na zewnątrz. Zadanie staje się Faulted, a wyjątek jest zachowany wewnątrz. Można go uzyskać:

  • Oczekując zakończenia za pomocą await (albo przez task.Wait()/task.Result — ale lepiej tego nie robić);
  • Sprawdzając właściwość task.Exception — tam będzie AggregateException.

Przykład


// Asynchroniczna metoda z błędem
async Task FailAsync()
{
    await Task.Delay(100);
    throw new InvalidOperationException("Coś poszło nie tak");
}

async Task MainAsync()
{
    try
    {
        await FailAsync();
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Błąd: {ex.Message}");
    }
}

Jeśli nie użyjesz try-catch, program upadnie. Jeśli użyjesz — wyjątek zostanie poprawnie złapany, nawet jeśli błąd nastąpił w innej task.

AggregateException: błędy hurtowo

Przy await Task.WhenAll(tasks) błędy z wielu zadań są zbierane do jednego AggregateException (w jego InnerExceptions).


async Task MultiFailAsync()
{
    Task t1 = Task.Run(() => throw new InvalidOperationException("Błąd 1"));
    Task t2 = Task.Run(() => throw new ArgumentException("Błąd 2"));
    try
    {
        await Task.WhenAll(t1, t2);
    }
    catch (Exception ex)
    {
        if (ex is AggregateException agg)
        {
            foreach (var e in agg.InnerExceptions)
                Console.WriteLine($"Wyjątek: {e.Message}");
        }
        else
        {
            Console.WriteLine($"Pojedynczy błąd: {ex.Message}");
        }
    }
}

Uwaga: przy użyciu await Task.WhenAll(tasks) .NET „rozwija” AggregateException, i w catch dostaniesz „pierwszy” wyjątek. Pełna lista jest dostępna przez task.Exception.InnerExceptions, jeśli zadanie zakończyło się z błędem.

3. „Fire and Forget”: dlaczego nie można po prostu zapomnieć o zadaniu

„Uruchom i zapomnij” często prowadzi do ukrytych awarii. Przykład:


Task.Run(() => { throw new Exception("Bach!"); }); // Błąd "odleci" w pustkę.

Nowoczesny runtime przechowuje błąd w zadaniu i może zakończyć proces z powodu nieobsłużonego wyjątku. Najlepsze podejście — przechować referencję do zadania i/lub subskrybować zdarzenie TaskScheduler.UnobservedTaskException.

Jak poprawnie?

  • Zachowuj zadanie, żeby poczekać na jego zakończenie i obsłużyć błąd;
  • Dla fire‑and‑forget złóż handler bezpośrednio wewnątrz delegata.

Task.Run(() => {
    try 
    {
        // Kod, który może wyrzucić wyjątek
    }
    catch (Exception ex)
    {
        // Logujemy, powiadamiamy, ale nie wypuszczamy błędu na zewnątrz
        Console.WriteLine($"Błąd w fire-and-forget: {ex.Message}");
    }
});

4. Błędy w wątkach (Thread): „łapać nie ma czego?”

Wyjątek w nowym Thread nie można złapać try-catch z zewnątrz — tylko wewnątrz ciała wątku.


var thread = new Thread(() =>
{
    try
    {
        throw new Exception("Błąd w wątku");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Złapano błąd wewnątrz wątku: {ex.Message}");
    }
});
thread.Start();

Jeśli nie umieścisz handlera, wyjątek zakończy tylko ten wątek (jeśli jest on background: thread.IsBackground = true). Dla wątków niefonowych nieobsłużony wyjątek może zakończyć cały proces. Zawsze umieszczaj try-catch wewnątrz wątku.

Jak zwracać wynik i błędy z wątku?

  • Używaj kolejek/kolekcji do przekazywania wyników/błędów;
  • Model zdarzeniowy;
  • Lepiej przejdź na zadania — z nimi wygodniej obsługiwać błędy.

5. Pętle równoległe: łapiemy błędy po‑inaczej

W pętlach równoległych błędy z różnych gałęzi agregują się w AggregateException.


try
{
    Parallel.For(0, 5, i =>
    {
        if (i % 2 == 0)
            throw new Exception($"Błąd w iteracji {i}");
    });
}
catch (AggregateException ex)
{
    foreach (var e in ex.InnerExceptions)
        Console.WriteLine($"[Pętla równoległa] Błąd: {e.Message}");
}

Jeśli trzeba logować lokalne awarie i kontynuować pozostałe gałęzie, umieść wewnętrzne try-catch w każdej gałęzi.

6. Obsługa błędów przy anulowaniu zadań

Przy anulowaniu przez CancellationToken zgodnie z konwencją rzucany jest OperationCanceledException — to nie awaria, tylko normalne zatrzymanie.


async Task DoWorkAsync(CancellationToken token)
{
    for (int i = 0; i < 10; i++)
    {
        token.ThrowIfCancellationRequested();
        await Task.Delay(100);
    }
}

// Gdzieś w kodzie:
var cts = new CancellationTokenSource();
var task = DoWorkAsync(cts.Token);
cts.Cancel(); // Około po 200 ms

try
{
    await task;
}
catch (OperationCanceledException)
{
    Console.WriteLine("Operacja została anulowana!");
}

Poza ThrowIfCancellationRequested(), wiele metod wspierających token (np. Task.Delay, HttpClient.GetAsync) samo rzuci OperationCanceledException. Sprawdzaj wsparcie dla anulowania.

7. Przydatne niuanse

Podejścia dla wszystkich przypadków

  • Umieszczaj try-catch na najwyższym poziomie asynchronicznych metod — tak złapiesz „uciekające” błędy.
  • Nie ignoruj zadań: zachowuj referencje, czekaj na zakończenie, loguj błędy.
  • Dla operacji równoległych (Task.WhenAll / Parallel.ForEach) uwzględniaj AggregateException.
  • Oddziel anulowanie od awarii: łap OperationCanceledException osobno.
  • Loguj błędy, szczególnie „ciche”, które nie zabijają aplikacji.
  • Zachowuj szczegóły: loguj wszystkie zagnieżdżone wyjątki, nie tylko pierwszy.
  • Obsługuj błędy „na miejscu”: jeśli nie wiesz co zrobić — przynajmniej zaloguj.

Gdzie łapać błąd w kodzie wielowątkowym i asynchronicznym

flowchart TD
    A[Wątek główny / UI] -->|Uruchamia zadanie| B[Task/async]
    B -->|Wewnątrz zadania| C[try/catch wewnątrz asynchronicznej metody]
    B -->|await w wątku głównym| D[try/catch wokół await]
    A -->|Uruchamia Thread| E[Thread]
    E -->|Wewnątrz| F[try/catch wewnątrz wątku]
    B -->|Wiele zadań| G[Task.WhenAll / Parallel.ForEach]
    G -->|Błąd| H[AggregateException]

8. Typowe błędy i „pułapki”

Błąd nr 1: oczekiwanie na zadanie przez .Result lub .Wait(). Możliwy deadlock i/lub nieoczekiwany AggregateException.

Błąd nr 2: uruchomienie fire‑and‑forget bez wewnętrznego try-catch — zadanie może cicho upaść, diagnostyki nie będzie.

Błąd nr 3: pominięte błędy w pętlach równoległych — część pracy nie została wykonana, a ty tego nie zauważysz.

Błąd nr 4: nieoddzielenie anulowania (OperationCanceledException) od prawdziwych błędów.

Błąd nr 5: logowanie tylko pierwszego błędu spośród wielu zadań — pozostałe zostają „w cieniu”.

Błąd nr 6: ponowne używanie jednego obiektu Exception dla wszystkich zadań — każdy błąd powinien mieć swoją instancję.

Błąd nr 7: brak obsługi wyjątków w wątku UI — błąd w tle pozostaje niezauważony, interfejs zachowuje się „duchowo”.

1
Ankieta/quiz
Wyjątki w klasycznych Thread, poziom 61, lekcja 4
Niedostępny
Wyjątki w klasycznych Thread
Obsługa błędów w asynchronicznym kodzie
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION