1. はじめに
まず重要な問いから始めましょう:なぜ非同期メソッドをただ信用して「全部うまくいくはず」と期待してはいけないのでしょうか?ファイル操作は例外を投げることがよくあります — ファイルが削除されているかもしれない、ディスク容量が足りない、権限がない、ファイルがロックされているなど。同期コードなら慣れたtry-catchブロックで問題を捕まえます。非同期コードでも考え方は同じですが、ニュアンスがあります:エラーはメソッド呼び出し直後ではなく、実際に操作が行われる後になって発生することがあります。
エラーが発生するタイミング
同期コードでStreamReader.Read()を使って読み取ると、例外は呼び出し行で直接飛びます — catchで捕まえておけばOKです。
非同期コードでは(await stream.ReadAsync())、エラーは操作開始時ではなく、タスクが失敗して完了したときにawaitの地点で発生します。もしawaitを付け忘れると、エラーはしばらく「見えない」ままになることがあります。
2. 非同期メソッドで例外を捕まえる方法
典型的なテンプレートをすぐに見てみましょう:
try
{
using FileStream fs = new FileStream("myfile.txt", FileMode.Open);
byte[] buffer = new byte[1024];
int bytesRead = await fs.ReadAsync(buffer, 0, buffer.Length);
// さらに処理...
}
catch (IOException ex)
{
Console.WriteLine("入出力エラー: " + ex.Message);
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("ファイルへのアクセス権がありません: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("未知のエラー: " + ex);
}
そう、シンプルです — 見慣れたtry-catchを非同期メソッド内で使います。重要なのは、そのメソッド自体がasyncであることです。そうしないとコンパイラが怒ります。
重要なポイント: awaitをどこに置くか
Task<int> readTask = fs.ReadAsync(buffer, 0, buffer.Length);
// ... ここでうっかり await や処理を忘れる
この場合、もしエラーが起きたらそれはタスク自体(Task)に入ります。結果を取得するまで — 例えばawait readTaskやTask.Exceptionを通して — あなたはそのエラーに気づきません。もし完全にawaitを忘れていたら、タスクは失敗しても誰にも知らせません。
3. なぜエラー処理なしの非同期コードは厄介なのか
シナリオ1: 「Fire and forget」— 初心者の罠
FileStream fs = new FileStream("file.txt", FileMode.Open);
byte[] buffer = new byte[8000];
fs.ReadAsync(buffer, 0, buffer.Length);
// その後プログラムは普通に動き続ける
読み取り操作は「並行で泳ぎに行く」ような状態になり、もしそれが失敗しても、どのcatchもその例外を捕まえません。例外はタスク内部に隠れます。このパターンは「fire and forget」と呼ばれ、実アプリでは重大なエラーの見逃しやリソースリークを招きます。
シナリオ2: awaitなしの非同期メソッド
Task t = MyAsyncMethod();
// ... ここでいろいろやって、その後 t を忘れる
MyAsyncMethod 内で発生したエラーは、あなたが明示的にタスクを待たない限り(await t や t.Wait())表に出てきません。
4. 非同期タスクのエラーを正しく捕まえる方法
戦略1: いつもawaitを使う
try
{
await SomeFileOperationAsync();
}
catch (Exception ex)
{
Console.WriteLine("何かがうまくいかなかった: " + ex.Message);
}
こうすると例外は待ちの場所で投げられ、失われません。
戦略2: .ContinueWithでの処理
何らかの理由でawaitを使わない場合、ContinueWithでエラーハンドラを付けることができます:
var task = fs.ReadAsync(buffer, 0, buffer.Length);
task.ContinueWith(t =>
{
if (t.Exception != null)
Console.WriteLine("非同期読み取りでのエラー: " + t.Exception.InnerException);
}, TaskContinuationOptions.OnlyOnFaulted);
正直に言うと、現代のC#アプリではあまり使われません — async/awaitの方がコードをシンプルで読みやすくします。
ファイル操作の非同期で起こりうる例外
- IOException — ディスク障害、ファイルが見つからない、パスが長すぎる、デバイスが利用不可、など。
- UnauthorizedAccessException — 権限不足。
- ObjectDisposedException — 操作終了前にストリームが閉じられた。
- OperationCanceledException — CancellationToken を使って操作がキャンセルされた。
5. 例: エラー処理付きの非同期読み取り
このロジックをアプリに追加してみます:
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($"読み取ったバイト数: {bytesRead} バイト, ファイル: {path}");
}
catch (FileNotFoundException ex)
{
Console.WriteLine("ファイルが見つかりません: " + ex.Message);
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("ファイルへのアクセス権がありません: " + ex.Message);
}
catch (IOException ex)
{
Console.WriteLine("読み取り/書き込みエラー: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("その他のエラー: " + ex.Message);
}
}
}
}
重要な注意
もしMainでawaitを使わず、ただTask result = SomeAsyncMethod(); のようにしていると — エラーは「黙る」ことになり、結果を取得しようとしたときに初めて出てきます。
6. エラー処理でよくあるミスと「踏み抜きやすい地雷」
非同期メソッドに対してawaitを付け忘れる — エラーが適切なタイミングで浮上せず、プログラムの挙動が予測不能になる。
非同期呼び出しをtry-catchで囲まない — アプリが最初の障害でクラッシュする。
汎用的なExceptionだけを捕まえて、UnauthorizedAccessExceptionやOperationCanceledExceptionのような特有の例外を無視する — 結果として診断が難しくなる。
ログなしで「fire and forget」タスクを使う — 例外がTaskの中に隠れてしまう。
タスクが完了したからといって成功したと考えるのは誤り。結果は明示的に待って(await)例外を処理する必要がある。
GO TO FULL VERSION