1. Wprowadzenie
Zacznijmy od ważnego pytania: dlaczego nie można po prostu zaufać metodom asynchronicznym i mieć nadzieję, że wszystko zawsze zadziała poprawnie? Operacje na plikach często rzucają wyjątki — plik mógł zostać usunięty, skończyła się pamięć, brakuje uprawnień albo plik jest zablokowany. W kodzie synchronicznym złapalibyście problemy w znanym bloku try-catch. W kodzie asynchronicznym filozofia jest taka sama, ale są niuanse: błąd może wystąpić nie od razu przy wywołaniu metody, lecz później, gdy operacja rzeczywiście się wykona.
Miejsce wystąpienia błędu
W kodzie synchronicznym przy czytaniu przez StreamReader.Read() wyjątek poleci bezpośrednio w linii wywołania — złapaliście w catch i OK.
W kodzie asynchronicznym (await stream.ReadAsync()) błąd spadnie nie w momencie startu operacji, lecz bezpośrednio przy await — kiedy task zakończy się z błędem. Jeśli zapomnicie postawić await, błąd może pozostać „niewidoczny” przez jakiś czas.
2. Jak łapać wyjątki w metodach asynchronicznych
Zaraz pokażemy typowy szablon:
try
{
using FileStream fs = new FileStream("myfile.txt", FileMode.Open);
byte[] buffer = new byte[1024];
int bytesRead = await fs.ReadAsync(buffer, 0, buffer.Length);
// Dalsze przetwarzanie...
}
catch (IOException ex)
{
Console.WriteLine("Błąd wejścia-wyjścia: " + ex.Message);
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("Brak dostępu do pliku: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("Nieznany błąd: " + ex);
}
Tak, tak, wszystko proste — używamy znanego try-catch, ale w metodzie asynchronicznej. Ważne, żeby sama metoda miała słowo kluczowe async, inaczej kompilator się zdenerwuje.
Ważny niuans: gdzie umieścić await
Task<int> readTask = fs.ReadAsync(buffer, 0, buffer.Length);
// ... tu przypadkowo zapomnieliśmy await albo obsługę
W takim przypadku błąd, jeśli się pojawi, trafi do samego zadania (Task), i nie dowiecie się o nim, dopóki nie spróbujecie dostać wyniku — np. przez await readTask lub przez właściwość Task.Exception. Jeśli w ogóle zapomnieliście o await — task może zakończyć się z błędem, a nikt wam o tym nie powie.
3. Dlaczego bez obsługi błędów kod asynchroniczny jest podstępny
Scenariusz 1: „Fire and forget” — pułapka dla początkujących
FileStream fs = new FileStream("file.txt", FileMode.Open);
byte[] buffer = new byte[8000];
fs.ReadAsync(buffer, 0, buffer.Length);
// A dalej program żyje swoim życiem
Operacja czytania wyrusza w „równoległy rejs”, i jeśli zakończy się błędem, żaden catch go nie złapie. Wyjątek schowa się wewnątrz taska. Ten wzorzec nazywa się „fire and forget” i w realnych aplikacjach grozi utratą krytycznych błędów i wyciekami zasobów.
Scenariusz 2: Metody asynchroniczne bez await
Task t = MyAsyncMethod();
// ... tu robimy coś, a potem zapomnieliśmy o t
Błędy, które wystąpiły wewnątrz MyAsyncMethod, nie wypłyną, dopóki wy wyraźnie nie poczekacie na zadanie (await t lub t.Wait()).
4. Jak poprawnie łapać błędy z zadań asynchronicznych
Strategia 1: Zawsze używaj await
try
{
await SomeFileOperationAsync();
}
catch (Exception ex)
{
Console.WriteLine("Coś poszło nie tak: " + ex.Message);
}
Dzięki temu wyjątek zostanie rzucony bezpośrednio w miejscu oczekiwania i nie zginie.
Strategia 2: Obsługa za pomocą .ContinueWith
Jeśli z jakiegoś powodu nie używacie await, można dodać handler błędów przez ContinueWith:
var task = fs.ReadAsync(buffer, 0, buffer.Length);
task.ContinueWith(t =>
{
if (t.Exception != null)
Console.WriteLine("Błąd przy asynchronicznym czytaniu: " + t.Exception.InnerException);
}, TaskContinuationOptions.OnlyOnFaulted);
Szczerze? W nowoczesnych aplikacjach C# rzadko się to robi — async/await robi kod prostszym i czytelniejszym.
Możliwe wyjątki przy pracy z plikami asynchronicznie
- IOException — awaria dysku, plik nie znaleziony, zbyt długa ścieżka, urządzenie niedostępne.
- UnauthorizedAccessException — niewystarczające uprawnienia.
- ObjectDisposedException — stream został zamknięty przed zakończeniem operacji.
- OperationCanceledException — operacja anulowana przez token anulowania (CancellationToken).
5. Przykład: Asynchroniczne czytanie z obsługą błędów
Dodajmy tę logikę do naszej aplikacji:
using System;
using System.IO;
using System.Threading.Tasks;
namespace FileAsyncDemo
{
class Program
{
static async Task Main()
{
string path = "bigfile.txt";
byte[] buffer = new byte[4096];
try
{
using FileStream fs = new FileStream(path, FileMode.Open);
int bytesRead = await fs.ReadAsync(buffer, 0, buffer.Length);
Console.WriteLine($"Przeczytano {bytesRead} bajtów z {path}");
}
catch (FileNotFoundException ex)
{
Console.WriteLine("Plik nie znaleziony: " + ex.Message);
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("Brak dostępu do pliku: " + ex.Message);
}
catch (IOException ex)
{
Console.WriteLine("Błąd odczytu/zapisu: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("Inny błąd: " + ex.Message);
}
}
}
}
Ważna uwaga
Jeśli nie używacie await w Main, a robicie tylko Task result = SomeAsyncMethod(); — błędy będą „milczeć” i pojawią się później, gdy jednak spróbujecie dostać wynik.
6. Najczęstsze błędy i „śliskie skórki” przy obsłudze błędów
Zapominanie o await na metodzie asynchronicznej — błędy nie wypływają na czas, program zachowuje się nieprzewidywalnie.
Nie owijać wywołań asynchronicznych w try-catch — aplikacja pada przy pierwszym błędzie.
Obsługiwać tylko Exception, ignorując specyficzne wyjątki typu UnauthorizedAccessException czy OperationCanceledException — w efekcie kiepska diagnostyka.
Używać „fire and forget” bez jawnego logowania błędów — wyjątki giną wewnątrz Task.
Sądzić, że jeśli task się zakończył — to wszystko OK. Trzeba wyraźnie czekać na wynik (await) i obsługiwać wyjątki.
GO TO FULL VERSION