WPFで未処理の例外を処理するベストプラクティス

2023/03/08に公開

はじめに

WPFで未処理の例外を処理する方針が、毎回わからなくなるので、ここに整理しておきます。

ベストプラクティス

つぎのようにApp.xamlのStartupにイベントハンドラーを追加して

<Application ...
             Startup="App_OnStartup">

App_OnStartupで、つぎのように処理すると扱いやすいことが多いです(詳細は後述)。

private void App_OnStartup(object sender, StartupEventArgs e)
{
    DispatcherUnhandledException += (o, args) =>
    {
        // ログ出力処理
        args.Handled = true; // 例外処理の中断
        if ( /** アプリケーションを継続可能か判定する **/ )
        {
            // アプリケーションが継続実行可能な場合
            // ユーザーへの適切な通知処理など
        }
        else
        {
            // アプリケーションが継続不可能な場合
            // ユーザーへ適切に通知したのち、リソースを解法してエラーとしてアプリケーションを終了する
            Environment.Exit(1);
        }

    };
    AppDomain.CurrentDomain.UnhandledException += (o, args) =>
    {
        // ユーザーへ適切に通知したのち、リソースを解法してエラーとしてアプリケーションを終了する
        Environment.Exit(1);
    };

    TaskScheduler.UnobservedTaskException += (o, args) =>
    {
        // ログ出力の実装
        args.SetObserved();
    };
}

AppのStartupの前で発生する例外(たとえばコンテナーの初期化とか)は、未処理にならないよう最新の注意を払って例外処理を実装しましょう。

Startup前には重たい処理を入れると、メインウィンドウの表示が遅れるます。そのため重たい処理は行わず、非同期処理はできるだけ使わないで、try-catchブロックで処理すると良いかと思います。

Application.DispatcherUnhandledException

基本的にはApplication.DispatcherUnhandledExceptionで例外を処理します。Application.DispatcherUnhandledExceptionでは例外チェーンを中断できますが、それ以外では中断できないためです。

この中でログを出力して、アプリケーションの実行を継続するか、それとも終了するか判断します。

継続する場合は、必要に応じてユーザーに適切に通知します。

終了する場合は、ユーザーへ通知した上で、リソースの解法処理をした上で、アプリケーションを終了します。

このとき、Environment.Exit(1)を呼び出すことで、Windowsのアプリケーションのクラッシュダイアログの表示を抑制するとともに、AppDomain.CurrentDomain.UnhandledExceptionを呼び出されないようにします。

Environment.Exit(1)を呼び出さないと、つづいてAppDomain.CurrentDomain.UnhandledExceptionが呼び出されますが、例外の2重処理になりやすいため、明示的に終了してしまうのが好ましいでしょう。

AppDomain.CurrentDomain.UnhandledException

Application.DispatcherUnhandledExceptionでは、つぎのように、明示的に作成したThreadで発生した例外は補足できません。

var thread = new Thread(() =>
{
    throw new NotImplementedException();
});
thread.Start();

この場合は、AppDomain.UnhandledExceptionを利用して例外を補足します。

ただし、AppDomain.UnhandledExceptionでは例外処理の中断ができません。そのためユーザーに通知した上で、リソースを解放してアプリケーションを終了します。

このとき、Environment.Exit(1)を呼び出すことで、Windowsのアプリケーションのクラッシュダイアログの表示を抑制します。

TaskScheduler.UnobservedTaskException

つぎのようにTaskをasync/awaitせず、投げっぱなしでバックグラウンド処理した際に例外が発生した場合は、TaskScheduler.UnobservedTaskExceptionで補足します。

private void OnClick(object sender, RoutedEventArgs e)
{
    Task.Run(() =>
    {
        throw new NotImplementedException();
    });
}

ただTaskScheduler.UnobservedTaskExceptionは例外が発生しても即座にコールされないため注意が必要です。

ユーザーの操作とは無関係に、「いつか」発行されるため、ユーザーに通知したり、アプリケーションを中断しても混乱を招くだけです。

未処理の例外は全般的に、あくまで最終手段とするべきものですが、とくにTaskScheduler.UnobservedTaskExceptionは最後の最後の保険と考えて、ログ出力程度に留めておくのが良いでしょう。

その他の手段

前述以外につぎの2つの方法がありますが、特別な理由がない限りこれらを利用する必要はありません。

Dispatcher.UnhandledException

前述のApplication.DispatcherUnhandledExceptionの実体は実のところ、Dispatcher.UnhandledExceptionです。

違いは、Application.DispatcherUnhandledExceptionはUIスレッド以外からも登録できますが、Dispatcher.UnhandledExceptionはUIスレッドからしか実行できないという点のみです。これはApplication.DispatcherUnhandledExceptionを呼ぶと、内部でDispatcher.Invokeを利用してUIスレッド上でDispatcher.UnhandledExceptionを呼び出すように実装されているからです。

そのためApplication.DispatcherUnhandledExceptionをハンドリングしていれば、Dispatcher.UnhandledExceptionをハンドリングする必要は一切ありません。

AppDomain.FirstChanceException

これはマネージコード内で発生したすべての例外を、呼び出しチェーンにしたがって例外をスローしていく前に呼び出されます。

一見便利そうですが、標準ライブラリや、サードパーティライブラリの内部で発生して、途中でインターセプトされて適切に処理された例外も補足してしまいます。そのため、アプリケーションでこれを補足すると、過剰な振る舞いになってしまいます。

表面上、例外が発生していないのに、どこかで握りつぶしてしまっていて不具合が隠ぺいされてしまっているような場合に、一時的に利用して原因の特定を行うような場合には利用できるかもしれません。

Discussion