1. What is "fire and forget"?
In programming the term fire and forget means starting a task without waiting for its completion. In the C# and .NET world this is most often done with Task instances that are started but never awaited (await), no reference is kept and they're effectively forgotten.
// The button starts a background task, but it's never awaited.
button.Click += (s, e) =>
{
Task.Run(() => DolgayaOperatsiya());
};
Sounds tempting: "let it run in the background while I do my thing". But with this approach, if an exception happens inside the task, nobody will learn about it in time — it will quietly get lost.
2. How exception handling works in Task
Classic: await and error handling
The standard way to work with async tasks is via await. If an error occurs in the task, it will be rethrown at the await point:
try
{
await SomeOperationAsync(); // if there is an Exception inside, it will surface to catch
}
catch(Exception ex)
{
Console.WriteLine("Whoops! The task failed: " + ex.Message);
}
So when you await a task you won't miss the exception.
But fire-and-forget tasks are not awaited!
public void StartWithoutWaiting()
{
// The task runs on its own. Nobody awaits it...
Task.Run(() => {
// Somewhere inside trouble happens:
throw new InvalidOperationException("Oy, everything's lost!");
});
// The method finished, the task runs silently in the background.
}
If an exception occurs inside such a task, it will not be thrown on the main thread. The app keeps running as if nothing happened.
Important
In .NET a task with an unhandled exception moves to the Faulted state. But if you don't observe it (await, .Result, .Wait(), etc.), nobody reads the exception and it doesn't show up in the caller code.
What actually happens "under the hood"?
For tasks nobody awaits, the only remaining chance to be noticed is the TaskScheduler.UnobservedTaskException event. It fires when the garbage collector (GC) finds a task with an unobserved exception. But this doesn't happen immediately and not where you expect — you can't rely on it.
3. Demonstration: Fire-and-forget error
// Example: starting a fire-and-forget task right from Main
using System;
using System.Threading.Tasks;
class Program
{
static void Main(string[] args)
{
FireAndForgetExample();
Console.WriteLine("Main thread keeps working...");
// Give the task time to finish
Task.Delay(2000).Wait();
}
static void FireAndForgetExample()
{
Task.Run(() =>
{
Console.WriteLine("Fire-and-forget task started!");
Task.Delay(500).Wait();
throw new InvalidOperationException("Error inside fire-and-forget task!");
});
}
}
If you run this code, nothing special will happen. The error occurs, but the program doesn't know about it. Sometimes you might see a warning in the IDE Output Window, but the user gets no information.
Why is this dangerous in real projects?
- Hard-to-reproduce bugs ("sometimes it fails — no idea why").
- Silent loss of data or logic (for example, an email wasn't sent).
- In production — no signals about issues unless logging is configured.
4. Correct ways to handle errors in fire-and-forget
Logging and handling errors inside the task itself
The minimal safe level — catch exceptions right inside the fire-and-forget task:
Task.Run(() =>
{
try
{
// Your long/risky code
throw new InvalidOperationException("Something went wrong!");
}
catch (Exception ex)
{
// Log the error or notify the user
Console.WriteLine("Fire-and-forget: caught exception: " + ex.Message);
// You can write to a log file, use an alerting system, etc.
}
});
Async void methods (and why you shouldn't do this)
async void DangerousFireAndForget()
{
// Something risky
throw new Exception("Boom!");
}
async void methods are essentially fire-and-forget: you can't wait for them, they don't return a Task. Exceptions from them go to the app-global handler (for example, AppDomain.UnhandledException) and can often crash the process. Use async void only for event handlers — and even then with care.
Using helper methods to handle errors
It's convenient to extract safe fire-and-forget startup into a wrapper:
// Generic method for safely starting a fire-and-forget task
public static void RunSafeFireAndForget(Func<Task> taskFactory)
{
Task.Run(async () =>
{
try
{
await taskFactory();
}
catch (Exception ex)
{
// Log the exception
Console.WriteLine("Fire-and-forget (safe): " + ex);
// You can add sending to monitoring here!
}
});
}
// Usage:
RunSafeFireAndForget(async () =>
{
await Task.Delay(1000);
throw new InvalidOperationException("Inside fire-and-forget!");
});
Real-life example: sending email
// Send button:
private void buttonSend_Click(object sender, EventArgs e)
{
Task.Run(() => SendEmail());
}
// Send method:
private void SendEmail()
{
try
{
// There could be real sending here
throw new Exception("SMTP-server is unavailable!");
}
catch (Exception ex)
{
// Logging
File.AppendAllText("errors.log", $"Send error: {ex.Message}\n");
}
}
5. What about UnobservedTaskException?
As a last resort, .NET provides the TaskScheduler.UnobservedTaskException event. It is called if a task faulted, nobody awaited it, and the task object was collected by GC. Relying on this is a bad idea — it's a "last-chance" mechanism.
TaskScheduler.UnobservedTaskException += (sender, e) =>
{
Console.WriteLine("Global UnobservedTaskException: " + e.Exception);
e.SetObserved(); // Don't forget to call this, otherwise the app may terminate!
};
More: TaskScheduler.UnobservedTaskException.
6. Useful nuances
Schematic comparison of approaches
| Approach | Exceptions handled? | Where to catch errors | Risk of "losing" an error |
|---|---|---|---|
|
Yes | In the caller | Low |
| Fire-and-forget without try/catch | No | Nowhere | Very high |
| Fire-and-forget with try/catch | Yes | Inside the task itself | Low (if you log) |
| async void method | No (goes to global) | Global handler | High |
How to design fire-and-forget correctly
- If the result or state of the task is critical — don't do fire-and-forget. Use await or keep the Task to await it later.
- Fire-and-forget is only justified for truly non-critical background work (e.g., telemetry sending).
- Always wrap fire-and-forget in your own method and catch/log exceptions.
- For complex background scenarios use queues and workers: Hangfire, Quartz.NET.
Practical use and interviews
In interviews you may be asked: "What happens if an exception occurs in a fire-and-forget task?" or "Why can't we use async void everywhere?" The right answer: you're responsible for the fate of errors in background tasks — either catch, log and analyze them, or get phantom bugs.
Mapping "fire-and-forget" vs await
| Scenario | Reliability of error handling | Applicability |
|---|---|---|
| Regular await | Excellent | Where a result is needed or success/fail matters |
| Fire-and-forget | Poor (if not handled manually) | Only for truly background and non-critical tasks |
| Fire-and-forget with try/catch | Good (if you log) | Background tasks where result isn't needed but failures should be known |
In the next lecture we'll discuss error handling in parallel tasks that return multiple results. Meanwhile remember: if you "fire" something, make sure it actually "hits" the target!
7. Typical mistakes when working with fire-and-forget tasks
Mistake #1: Ignoring exceptions in fire-and-forget.
Beginners hope exceptions will "bubble up" somewhere. Without try-catch and logging they get lost, causing undetectable bugs.
Mistake #2: Using async void outside of event handlers.
Such methods throw exceptions to the global handler (e.g., AppDomain.UnhandledException), which can crash the app.
Mistake #3: Over-catching exceptions.
Catching all exceptions inside the task can hide problems that would be better handled by the caller, making debugging harder.
Mistake #4: Neglecting logging.
Without logging errors in fire-and-forget tasks you can't know about failures, especially in production.
GO TO FULL VERSION