1. Introduzione
Iniziamo con una domanda importante: perché non è possibile fidarsi semplicemente dei metodi asincroni e sperare che tutto funzioni sempre alla perfezione? Le operazioni sui file spesso lanciano eccezioni — il file potrebbe essere stato eliminato, la memoria finita, permessi insufficienti o il file bloccato. Nel codice sincrono cattureresti i problemi nel solito blocco try-catch. Nel codice asincrono la filosofia è la stessa, ma ci sono sfumature: l'errore può verificarsi non immediatamente alla chiamata del metodo, ma più tardi, quando l'operazione viene effettivamente eseguita.
Momento in cui nasce l'errore
Nel codice sincrono, leggendo con StreamReader.Read(), l'eccezione verrà sollevata direttamente sulla riga di chiamata — la prendi nel catch e tutto ok.
Nel codice asincrono (await stream.ReadAsync()) l'errore cadrà non al momento dell'avvio dell'operazione, ma proprio al await — quando il Task termina con un errore. Se dimentichi di mettere il await, l'errore può rimanere «invisibile» per un po'.
2. Come catturare le eccezioni nei metodi asincroni
Vediamo subito lo schema tipico:
try
{
using FileStream fs = new FileStream("myfile.txt", FileMode.Open);
byte[] buffer = new byte[1024];
int bytesRead = await fs.ReadAsync(buffer, 0, buffer.Length);
// Ulteriore elaborazione...
}
catch (IOException ex)
{
Console.WriteLine("Errore di input/output: " + ex.Message);
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("Accesso al file negato: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("Errore sconosciuto: " + ex);
}
Sì, tutto semplice — usiamo il familiare try-catch, ma nel metodo asincrono. È importante che il metodo stesso abbia la parola chiave async, altrimenti il compilatore si arrabbierà.
Dettaglio importante: dove mettere il await
Task<int> readTask = fs.ReadAsync(buffer, 0, buffer.Length);
// ... qui per sbaglio abbiamo dimenticato await o la gestione
In tal caso l'errore, se succede, finirà nel Task stesso (Task), e non lo saprai finché non cercherai di ottenere il risultato — per esempio con await readTask o usando la proprietà Task.Exception. Se ti dimentichi completamente del await — il Task può terminare con un errore e nessuno te lo dirà.
3. Perché il codice asincrono è insidioso senza gestione degli errori
Scenario 1: «Fire and forget» — la trappola del principiante
FileStream fs = new FileStream("file.txt", FileMode.Open);
byte[] buffer = new byte[8000];
fs.ReadAsync(buffer, 0, buffer.Length);
// E poi il programma continua per la sua strada
L'operazione di lettura va in «navigazione parallela», e se termina con un errore, nessun catch la prenderà. L'eccezione rimarrà nascosta dentro il Task. Questo pattern si chiama «fire and forget» e nelle applicazioni reali può causare perdita di errori critici e perdite di risorse.
Scenario 2: Metodi asincroni senza await
Task t = MyAsyncMethod();
// ... qui facciamo qualcosa e poi ci dimentichiamo di t
Gli errori avvenuti dentro MyAsyncMethod non saliranno in superficie finché non aspetti esplicitamente il Task (await t o t.Wait()).
4. Come gestire correttamente gli errori dai Task asincroni
Strategia 1: Usa sempre await
try
{
await SomeFileOperationAsync();
}
catch (Exception ex)
{
Console.WriteLine("Qualcosa è andato storto: " + ex.Message);
}
Così l'eccezione verrà lanciata proprio nel punto di attesa e non andrà persa.
Strategia 2: Gestione con .ContinueWith
Se per qualche motivo non usi await, puoi aggiungere un gestore errori tramite ContinueWith:
var task = fs.ReadAsync(buffer, 0, buffer.Length);
task.ContinueWith(t =>
{
if (t.Exception != null)
Console.WriteLine("Errore durante la lettura asincrona: " + t.Exception.InnerException);
}, TaskContinuationOptions.OnlyOnFaulted);
Onestamente? Nelle applicazioni C# moderne si usa raramente — async/await rende il codice più semplice e pulito.
Possibili eccezioni quando si lavora con file in modo asincrono
- IOException — guasto del disco, file non trovato, percorso troppo lungo, dispositivo non disponibile.
- UnauthorizedAccessException — permessi insufficienti.
- ObjectDisposedException — lo stream è stato chiuso prima del termine dell'operazione.
- OperationCanceledException — l'operazione è stata annullata tramite un token di cancellazione (CancellationToken).
5. Esempio: Lettura asincrona con gestione degli errori
Aggiungiamo questa logica alla nostra applicazione:
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($"Letti {bytesRead} byte da {path}");
}
catch (FileNotFoundException ex)
{
Console.WriteLine("File non trovato: " + ex.Message);
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("Accesso al file negato: " + ex.Message);
}
catch (IOException ex)
{
Console.WriteLine("Errore di lettura/scrittura: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("Altro errore: " + ex.Message);
}
}
}
}
Nota importante
Se non usi await in Main, ma fai solo Task result = SomeAsyncMethod(); — gli errori rimarranno «silenziosi» e si manifesteranno più tardi, quando proverai a ottenere il risultato.
6. Errori più comuni e «trappole» nella gestione degli errori
Dimenticare di mettere await su un metodo asincrono — le eccezioni non emergono in tempo, il programma si comporta in modo imprevedibile.
Non avvolgere le chiamate asincrone in try-catch — l'applicazione crolla al primo errore.
Gestire solo Exception, ignorando eccezioni specifiche come UnauthorizedAccessException o OperationCanceledException — in pratica perdi diagnostica utile.
Usare task «fire and forget» senza logging esplicito degli errori — le eccezioni si perdono dentro il Task.
Pensare che se il Task è terminato allora è tutto andato bene. Bisogna aspettare esplicitamente il risultato (await) e gestire le eccezioni.
GO TO FULL VERSION