1. Introduction
Let's start with an important question: why can't you just trust async methods and hope everything always works fine? File operations often throw exceptions — the file might have been deleted, you ran out of space, you don't have permissions, or the file is locked. In synchronous code you'd catch problems in a familiar try-catch block. In async code the philosophy is the same, but there are nuances: an error might not happen immediately when you call the method, but later when the operation actually runs.
When the error occurs
In synchronous code when reading via StreamReader.Read() the exception will be thrown right on the call line — you catch it in catch, and that's fine.
In async code (await stream.ReadAsync()) the error will surface not at the start of the operation, but at the moment of await — when the task completes with an error. If you forget to put await, the error can remain "invisible" for a while.
2. How to catch exceptions in async methods
Let's look at a common pattern right away:
try
{
using FileStream fs = new FileStream("myfile.txt", FileMode.Open);
byte[] buffer = new byte[1024];
int bytesRead = await fs.ReadAsync(buffer, 0, buffer.Length);
// Further processing...
}
catch (IOException ex)
{
Console.WriteLine("I/O error: " + ex.Message);
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("No access to the file: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("Unknown error: " + ex);
}
Yes — it's simple: use the familiar try-catch, but inside an async method. It's important that the method itself is marked with the async keyword, otherwise the compiler will complain.
Important nuance: where to put await
Task<int> readTask = fs.ReadAsync(buffer, 0, buffer.Length);
// ... here you accidentally forgot await or handling
In that case the error, if it happens, will go into the task itself (Task), and you won't know about it until you try to get the result — for example via await readTask or via the Task.Exception property. If you completely forget about await — the task may fail with an error and nobody will tell you.
3. Why async code is tricky without error handling
Scenario 1: "Fire and forget" — a beginner's trap
FileStream fs = new FileStream("file.txt", FileMode.Open);
byte[] buffer = new byte[8000];
fs.ReadAsync(buffer, 0, buffer.Length);
// And the program goes on with its life
The read operation goes off "in parallel", and if it completes with an error, no catch will catch it. The exception is hidden inside the task. This pattern is called "fire and forget" and in real apps it can lead to lost critical errors and resource leaks.
Scenario 2: Async methods without await
Task t = MyAsyncMethod();
// ... do something here, then forget about t
Errors that happen inside MyAsyncMethod won't surface until you explicitly await the task (await t or t.Wait()).
4. How to properly catch errors from async tasks
Strategy 1: Always use await
try
{
await SomeFileOperationAsync();
}
catch (Exception ex)
{
Console.WriteLine("Something went wrong: " + ex.Message);
}
This way the exception will be thrown right at the await point and won't get lost.
Strategy 2: Handling with .ContinueWith
If for some reason you don't use await, you can add an error handler via ContinueWith:
var task = fs.ReadAsync(buffer, 0, buffer.Length);
task.ContinueWith(t =>
{
if (t.Exception != null)
Console.WriteLine("Error during async read: " + t.Exception.InnerException);
}, TaskContinuationOptions.OnlyOnFaulted);
Honestly? In modern C# apps this is rare — async/await makes the code simpler and cleaner.
Possible exceptions when working with files asynchronously
- IOException — disk failure, file not found, path too long, device unavailable.
- UnauthorizedAccessException — insufficient permissions.
- ObjectDisposedException — the stream was closed before the operation finished.
- OperationCanceledException — the operation was canceled via a cancellation token (CancellationToken).
5. Example: Asynchronous read with error handling
Let's add this logic to our app:
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($"Read {bytesRead} bytes from {path}");
}
catch (FileNotFoundException ex)
{
Console.WriteLine("File not found: " + ex.Message);
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("No access to the file: " + ex.Message);
}
catch (IOException ex)
{
Console.WriteLine("Read/write error: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("Other error: " + ex.Message);
}
}
}
}
Important note
If you don't use await in Main, and only do something like Task result = SomeAsyncMethod(); — errors will "stay silent" and show up later when you finally try to get the result.
6. Most common mistakes and pitfalls in error handling
Forgetting to put await on an async method — errors don't surface in time, the program behaves unpredictably.
Not wrapping async calls in try-catch — the app crashes on the first failure.
Only catching Exception, ignoring specific exceptions like UnauthorizedAccessException or OperationCanceledException — results in poor diagnostics.
Using "fire and forget" tasks without explicit error logging — exceptions get lost inside the Task.
Assuming that if a task completed — everything succeeded. You need to explicitly await the result (await) and handle exceptions.
GO TO FULL VERSION