こんにちは、働くC#プログラマーのさんさめです。
GUIのアプリケーションを使ってもらうなら、
必ず実装すべきなのが例外終了時の処理です。
なぜなら、ユーザーの手元で起きてしまった例外を
アプリ制作者が知ることは往々にして難しいからです。
アプリが例外になってしまい、それを特にケアしていない場合、
ユーザーの手元では次のようなダイアログが現れます。
(PCの設定などによってはダイアログが出ずにいきなり終了することもあります)

この時ユーザーにできることは非常に限られます。
- 見なかったことにする。もう一度アプリを起動する
- 制作者に報告しつつもう一度アプリを起動する。
このうち、2を行なってくれる人はほぼ皆無です。
なぜなら、たいていのユーザーの頭の中では
- 制作者の連絡先なんて知らないよ…
- 頑張って報告なんてしても本当に直してもらえるのか分からない
- そもそも嫌な顔をされてしまうかも
…といった考えが先に浮かぶからです。
たとえば上記画像のようにExcelが異常終了やフリーズしてしまったとして、
Microsoftのサポートに報告をするでしょうか。
なかなかそこまでする人は少ないでしょう。
せいぜいその場でちょっと文句を言ってみて、
再度Excelを起動するだけです。
「ユーザーは異常終了を逐一報告してはくれない」ということを、
私のような業務アプリケーションを作る立場や、
個人でアプリを作る立場でも当然考慮する必要があります。
そのため、
「いかにユーザーの手元で起きてしまったバグを知ることができるか?」
というフローを作る必要があります。
幸いC#では、try~catch構文でキャッチしていない(=未処理の)例外が
発生してしまったときにイベントが発火されます。
本記事では未処理例外発生時のイベントを購読して、
ユーザーに例外をお知らせするウィンドウを実装する例を紹介します。
忙しい人のための全イベント実装例
詳しく中身を知らなくてもいいから、とにかく全部例外は捕捉したい!
という人は以下のコードをどうぞ。
できるだけエントリポイント直後で行うのが理想です。
WPFでいうとApp.xaml.csですね。
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// UIスレッドの未処理例外で発生
DispatcherUnhandledException += OnDispatcherUnhandledException;
// UIスレッド以外の未処理例外で発生
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
// それでも処理されない例外で発生
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
}
private void OnDispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
{
var exception = e.Exception;
HandleException(exception);
}
private void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
{
var exception = e.Exception.InnerException as Exception;
HandleException(exception);
}
private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
var exception = e.ExceptionObject as Exception;
HandleException(exception);
}
private void HandleException(Exception e)
{
// ログを送ったり、ユーザーにお知らせしたりする
MessageBox.Show($"エラーが発生しました\n{e?.ToString()}");
Environment.Exit(1);
}
}
未処理の例外が発生したときに起きるイベント
未処理の例外が発生したときに起きるイベントは
大きく分けて3種類あります。
- UIスレッドの例外で発火するDispatcherUnhandledException
- UIスレッド以外の例外で発火するUnobservedTaskException
- それでも処理されなかった時に発火するUnhandledException
ざっくり言ってしまうと、普通に書いた処理の実行中に起きた例外は、
1番目の DispatcherUnhandledException で購読できて、
非同期処理の実行中に起きた例外は、
2番目の UnobservedTaskException で購読できるということです。
3番目のUnhandledExceptionは上2つのイベント購読で、
例外を握りつぶす処理やアプリ終了の処理を
書かなかった場合に発生します。
1番目だけ使用することや2番目だけ使用することはほぼありません。
セットで使いましょう。
個別の使い方については、それぞれ記事がありますので、
そちらをお読みいただければと思います。
例外終了時にユーザーに何を見せるべきか?
さて、上記の全イベント実装例では、
MessagseBoxを使用して例外情報をそのままユーザーに見せています
(文言が微妙に違いますがご容赦下さい)

…スタックトレースが画面の大半を支配してしまっていますね。
スタックトレースは、プログラムを知らない人にはよく分からん英語の羅列であり、
かなりの圧迫感を与えることで有名です。
ましてやWPFのスタックトレースは長くなりがちなので…。
実際には文言や表示内容を工夫してユーザーフレンドリーにすると良いですね。
そもそも例外によってエラー終了している時点で、
ユーザーフレンドリーではないのです。
特に、最初に「申し訳ありません」と大きく書かれているかどうかで、
ユーザーの心象はだいぶ異なります。
結局のところ、アプリケーションを使ってもらうこととは、
アプリを通して人と人との付き合いをしていることなので、
画面の向こうの人を思いやって謝罪文を書きましょう。
例外情報を取得して不具合修正に活かす
本記事の冒頭の説明では、
「ユーザーはエラー終了のことを報告してくれない」
だから「報告しやすいウィンドウを作ろう」という語りだしでした。
ですが、実際にはユーザーからの報告に任せてしまうと、
なかなか例外の直接原因にはたどり着けません。
なぜなら、ユーザーによって報告のフォーマットはバラバラになってしまうからです。
- 「何もしてないのに壊れた」という人
- ウィンドウのスクショを送ってくる人
- そもそも報告してくれない人
これを、ユーザーのせいにしても事態は何も改善しません。
本来報告してくれるだけでもとてもありがたいのですから。
さて、実際にどう対応するかですが、
たとえば、例外内容をファイルに保存しておいたりすると、よりケアがしやすいです。
インフラが整っているのなら自動でサーバーにログ投稿するのは最高ですね。
ユーザーに報告してもらう、という手間をスキップできます。
また、軽い状況報告欄を作っておくのも良いですね。
再現確認の助けになる可能性があります。
なんにせよ、ユーザーはアプリケーションが落ちていることで、
すでに少なからず腹を立てているはずなので、
負担(=ユーザーがやらなければならないこと)はなるべく軽減しましょう。
まとめ
まとめです。
- アプリケーションが落ちた時、ユーザーは取る行動は様々
- 開発者としてはできるだけ不具合はすぐに直したい
- あらかじめイベントを購読しておくことで想定外のエラーに対応できる
- 例外情報を取得できるインフラを作って、
ユーザーの報告頼りにならない不具合対応環境を作ろう
最後までお読みいただき、ありがとうございました。
コメント
XamarinやMauiでも、同様に例外を把握したいです。
// UIスレッドの未処理例外で発生
DispatcherUnhandledException += OnDispatcherUnhandledException;
// UIスレッド以外の未処理例外で発生
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
このふたつ、XamarinやMauiのときはどうなりますか?
もしご存じでしたら記事にしていただけると嬉しいです。