1. Cos'è "fire and forget"?
In programmazione il termine fire and forget significa avviare un'operazione senza attendere il suo completamento. Nel mondo C# e .NET questo si fa spesso con Task che vengono lanciati ma non await-ati, non si mantiene il riferimento e praticamente vengono dimenticati.
// Il bottone avvia una task in background, ma da nessuna parte viene await-ata.
button.Click += (s, e) =>
{
Task.Run(() => DolgaiaOperaciia());
};
Suona comodo: "lascia che lavori in background e io faccio altro". Ma con questo approccio, se dentro la task si verifica un'eccezione, nessuno lo saprà in tempo — verrà silenziosamente persa.
2. Come funziona la gestione delle eccezioni in un Task
Classico: await e gestione degli errori
Il modo standard per lavorare con le task asincrone è tramite await. Se nella task accade un errore, verrà rilanciato nel punto di attesa:
try
{
await SomeOperationAsync(); // se qui dentro c'è un Exception, andrà nel catch
}
catch(Exception ex)
{
Console.WriteLine("Ops! Nella task è successo un errore: " + ex.Message);
}
Quindi quando aspetti il completamento della task, non perdi l'eccezione.
Ma le task "fire and forget" non vengono attese!
public void ZapustitBezOzhidaniia()
{
// La task è per conto suo. Nessuno la aspetta...
Task.Run(() => {
// Da qualche parte dentro succede un disastro:
throw new InvalidOperationException("Oi, tut vse propalo!");
});
// Il metodo è terminato, la task lavora silenziosamente in background.
}
Se dentro questa task si verifica un'eccezione, essa non verrà lanciata nel thread principale. L'app continuerà a funzionare come se nulla fosse successo.
Importante
In .NET una task con un'eccezione non gestita passa nello stato Faulted. Ma se non la await-i (await, .Result, .Wait() ecc.), nessuno leggerà l'eccezione e non si manifesterà nel codice chiamante.
Cosa succede "sotto il cofano"?
Per le task che nessuno aspetta, rimane un'unica possibilità per essere notate — l'evento TaskScheduler.UnobservedTaskException. Questo scatta quando il garbage collector (GC) rileva una task con eccezione non osservata. Ma ciò non avviene subito né nel punto in cui te lo aspetti — non si può fare affidamento su questo.
3. Dimostrazione: errore fire-and-forget
// Esempio: lanciamo una task fire-and-forget direttamente da Main
using System;
using System.Threading.Tasks;
class Program
{
static void Main(string[] args)
{
FireAndForgetExample();
Console.WriteLine("Il thread principale continua a lavorare...");
// Diamo tempo alla task di finire
Task.Delay(2000).Wait();
}
static void FireAndForgetExample()
{
Task.Run(() =>
{
Console.WriteLine("La task fire-and-forget è iniziata!");
Task.Delay(500).Wait();
throw new InvalidOperationException("Errore dentro la task fire-and-forget!");
});
}
}
Se esegui questo codice, ... non succederà nulla di apparente. L'errore accade, ma il programma non lo saprà. A volte si vede un warning nella Output Window dell'IDE, ma per l'utente — zero informazioni.
Perché è pericoloso nei progetti reali?
- Bug complessi difficili da riprodurre ("a volte non funziona — non si capisce perché").
- Perdita silenziosa di dati o logica (es. mail non inviata al cliente).
- In produzione — mancanza di segnali sui problemi se non è impostato il logging.
4. Modi corretti per gestire errori in fire-and-forget
Logging e gestione degli errori dentro la stessa task
Il livello minimo sicuro è catturare le eccezioni dentro la stessa task fire-and-forget:
Task.Run(() =>
{
try
{
// Il tuo codice lungo/pericoloso
throw new InvalidOperationException("Qualcosa è andato storto!");
}
catch (Exception ex)
{
// Logghiamo l'errore o informiamo l'utente
Console.WriteLine("Fire-and-forget: eccezione catturata: " + ex.Message);
// Si può scrivere su file di log, inviare ad un sistema di alert ecc.
}
});
Metodi async void (e perché non è una buona idea)
async void DangerousFireAndForget()
{
// Qualcosa di pericoloso
throw new Exception("Bdysh!");
}
I metodi async void sono di fatto fire-and-forget: non è possibile aspettarli, non restituiscono Task. Le eccezioni da essi vanno al gestore globale dell'app (es. AppDomain.UnhandledException) e spesso portano al crash del processo. Usa async void solo per gli event handler — e comunque con cautela.
Uso di helper per gestire errori
Comodo estrarre l'avvio sicuro di fire-and-forget in un wrapper:
// Metodo generico per lanciare in sicurezza un fire-and-forget
public static void RunSafeFireAndForget(Func<Task> taskFactory)
{
Task.Run(async () =>
{
try
{
await taskFactory();
}
catch (Exception ex)
{
// Logghiamo l'eccezione
Console.WriteLine("Fire-and-forget (safe): " + ex);
// Si può aggiungere l'invio a un sistema di monitoraggio!
}
});
}
// Utilizzo:
RunSafeFireAndForget(async () =>
{
await Task.Delay(1000);
throw new InvalidOperationException("Dentro il fire-and-forget!");
});
Esempio "reale": invio email
// Bottone per invio mail:
private void buttonSend_Click(object sender, EventArgs e)
{
Task.Run(() => SendEmail());
}
// Metodo di invio:
private void SendEmail()
{
try
{
// Qui potrebbe esserci l'invio reale
throw new Exception("SMTP-server nedostupen!");
}
catch (Exception ex)
{
// Logging
File.AppendAllText("errors.log", $"Oshibka otpravki: {ex.Message}\n");
}
}
5. E che dire di UnobservedTaskException?
Come ultima risorsa, .NET fornisce l'evento TaskScheduler.UnobservedTaskException. Viene chiamato se una task è terminata con errore, nessuno l'ha await-ata e l'oggetto task è stato raccolto dal GC. Non fare affidamento su questo — è un meccanismo di "ultima chance".
TaskScheduler.UnobservedTaskException += (sender, e) =>
{
Console.WriteLine("Globale UnobservedTaskException: " + e.Exception);
e.SetObserved(); // Non dimenticare di chiamarlo, altrimenti l'app può terminare in modo anomalo!
};
Per approfondire: TaskScheduler.UnobservedTaskException.
6. Dettagli utili
Confronto schematico degli approcci
| Metodo | Eccezioni gestite? | Dove catturare gli errori | Rischio di "perdere" l'errore |
|---|---|---|---|
|
Sì | Nel codice chiamante | Basso |
| Fire-and-forget senza try/catch | No | In nessun posto | Molto alto |
| Fire-and-forget con try/catch | Sì | Dentro la task stessa | Basso (se fai logging) |
| async void-metodo | No (va al globale) | Gestore globale | Alto |
Come progettare correttamente fire-and-forget
- Se il risultato o lo stato della task è critico — non usare fire-and-forget. Usa await o conserva il Task per aspettarlo successivamente.
- Fire-and-forget è giustificato solo per task di background davvero non critiche (es. invio di telemetria).
- Avvolgi sempre fire-and-forget in un metodo proprio e cattura/logga le eccezioni.
- Per scenari di background complessi usa code e worker: Hangfire, Quartz.NET.
Uso pratico e colloqui
Nei colloqui spesso chiedono: "Cosa succede se in una task fire-and-forget si verifica un'eccezione?" o "Perché non usare everywhere async void?" La risposta corretta: sei tu responsabile degli errori nelle task di background — o le catturi, le logghi e le analizzi, o ti ritrovi bug fantasma.
Confronto "fire-and-forget" vs await
| Scenario | Affidabilità nella gestione errori | Applicabilità |
|---|---|---|
| Await normale | Eccellente | Dovunque sia necessario il risultato o sia importante il successo/fallimento |
| Fire-and-forget | Pessima (se non gestita manualmente) | Solo per task davvero di background e non critiche |
| Fire-and-forget con try/catch | Buona (se logghi) | Task di background dove non serve il risultato ma è importante sapere dei fallimenti |
Nella prossima lezione discuteremo la gestione degli errori in attività parallele che restituiscono più risultati. Intanto ricordate: se avete "sparato" qualcosa, assicuratevi che sia arrivato a destinazione!
7. Errori tipici quando si lavora con fire-and-forget
Errore №1: Ignorare le eccezioni nei fire-and-forget.
I principianti sperano che le eccezioni "emergano da qualche parte". Senza try-catch e logging si perdono, portando a bug non rilevati.
Errore №2: Usare async void al di fuori degli event handler.
Questi metodi buttano le eccezioni al gestore globale (es. AppDomain.UnhandledException), il che può far terminare l'app in modo anomalo.
Errore №3: Gestire eccessivamente le eccezioni.
Catturare ogni eccezione dentro la task può nascondere problemi che sarebbe meglio gestire nel codice chiamante, rendendo il debug più difficile.
Errore №4: Trascurare il logging.
Senza log delle eccezioni nelle task fire-and-forget è impossibile sapere dei fallimenti, specialmente in produzione.
GO TO FULL VERSION