WPFでアプリ全体の例外を捕まえる方法

コンピュータ

アプリケーションのコード内で個別に例外を処理するのが面倒なので、
アプリケーション全体の例外を補足し処理する方法を調べてみました。

ソースコード

App.xaml.cs

using System.Diagnostics;
using System.Windows;
using System.Windows.Threading;

namespace WpfGlobalExceptionSample;

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        // UIスレッドの未処理例外
        DispatcherUnhandledException += OnDispatcherUnhandledException;

        Debug.Print("Application started");
    }
    // UIスレッドの未処理例外
    private void OnDispatcherUnhandledException(
        object sender,
        DispatcherUnhandledExceptionEventArgs e)
    {
        Debug.Print($"[GLOBAL UI EXCEPTION] {e.Exception}");

        // true にすると「アプリは落ちない」
        // 落とすほうが好み。
        // e.Handled = true;
    }

}

MainWindow.xaml

<Window x:Class="WpfGlobalExceptionSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfGlobalExceptionSample"
        mc:Ignorable="d"
        Title="Global Exception Sample"
        Width="400"
        Height="200">
    <StackPanel Margin="20" VerticalAlignment="Center">
        <Button Content="UIスレッド例外"
                Height="30"
                Margin="0,0,0,10"
                Click="Button_Click"/>

        <Button Content="Task例外 (await)"
                Height="30"
                Click="Button_Task_Click"/>
    </StackPanel>
</Window>

MainWindow.xaml.cs

using System;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Windows;

namespace WpfGlobalExceptionSample;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        try
        {
            ThrowLocalException();
        }
        catch (Exception ex)
        {
            Debug.Print($"[LOCAL CATCH] {ex.Message}");

            // ★ 握りつぶさず必ず再スロー
            throw;
        }
    }

    private void ThrowLocalException()
    {
        throw new InvalidOperationException("UIスレッドの例外です");
    }

    private async void Button_Task_Click(object sender, RoutedEventArgs e)
    {
        try
        {
            await Task.Run(() =>
            {
                throw new Exception("Task内の例外です");
            });
        }
        catch (Exception ex)
        {
            Debug.Print($"[LOCAL TASK CATCH] {ex.Message}");

            // 再スロー → DispatcherUnhandledException に届く
            throw;
        }
    }
}

実行例

  1. UIスレッド例外ボタンを押す。
  2. 個別でcatchされる。
  3. その後、全体でもcatchされる。

例外の流れ

UIスレッドの場合:

ThrowLocalException()
  ↓
catch 
  ↓
throw;
  ↓
DispatcherUnhandledException
  ↓
アプリ全体で一元処理

Taskの場合:

Task.Run 内で例外
  ↓
await 
  ↓
catch 
  ↓
throw;
  ↓
DispatcherUnhandledException

ちなみに、awaitしないTaskの例外は捕捉しないと思われます。

Task.Run(() => throw new Exception());

その場合UnobservedTaskExceptionで捕捉できるらしいですが、個人的に投げっぱなしTaskはあまり使わないので、未確認です。

あと、アプリケーション全体の例外処理ですが、回復できない例外が発生した場合、アプリケーションを強制終了するほうが、個人的好みです。

実運用のビジネスアプリでは、例外のたびにアプリが落ちるとクライアントからお叱りを受けるので、例外をログに記録し可能な限り続行する必要があるかもしれません。

しかし、実行時例外は想定外の可能性が高いので、強制終了しておいたほうが、傷が浅くて済むのではないかと考えます。

コメント