メインコンテンツまでスキップ

アプリケーションのクラッシュ

アプリケーションのクラッシュとは、アプリケーションが予期せず停止または動作不能になる事象を指します。クラッシュは、ユーザーエクスペリエンスに大きな影響を与えるため、アプリケーションの信頼性と安定性を確保するために重要な問題です。一方で、アプリケーションのクラッシュを防ごうとするあまり、過剰な例外処理を行うと、かえって状況を悪化させたり問題の調査や修正が困難になる場合があります。

アプリケーションが予期せず終了する状況のパターン

アプリケーションで問題が生じて予期せず終了する状況は以下の3つに大別できます。

  1. アプリケーション内で発生した例外が処理されず、未処理例外により異常終了する
  2. アプリケーション内で問題を検出して自己終了する
  3. アプリケーションの外部からプロセスが強制終了される

このうち2.のケースは本来「予期せず」という状況ではありませんが、不十分なログ、不適切な例外処理、ライブラリ内の自己終了など開発者が把握していないフローで終了に至るケースもよくあるため、これも含めています。

上記で挙げたもののうち、1.についてはWindows Error Reporting (WER) に例外が通知されてOSの機能でダンプファイルを採取することができるため、原因特定が容易かどうかはともかく、調査に向けた情報採取は比較的容易です。このケースではまずダンプ ファイルを採取して調査を行い、原因が特定できない場合は例外の種類や状況に応じて次の調査方法を検討します。

また、3.についてはSilent Process Exit Monitoringと呼ばれるOSの機能でアプリケーションを終了させたプロセスのダンプ ファイルを取得することができます。1.の場合と同様にダンプ ファイルから調査を進めることが可能ですが、一般的にはウィルス対策ソフトウェアなどアプリケーションと関連がない外部の製品であることが多いため、その場合は当該製品の開発元への問い合わせが必要です。

最も厄介な状況が2.のケースです。この場合OSの機能でダンプ ファイルを取得できないばかりでなく、アプリケーションが正常に終了した状況と区別できないことも多く、ダンプ ファイルの採取が困難です。再現手順が判明している場合は、デバッガーを接続した状態で事象を再現させながらライブデバッグしたり、Time Travel Debuggingトレースを採取して自己終了に至った経緯を追跡することで調査を進めることができます。しかし、再現手順が不明な場合や、デバッガーを接続できない場合、Time Travel Debuggingトレースを採取できない事情がある場合は、アプリケーションのログ出力箇所を追加したり不用意な例外処理を取り除いて被疑箇所を絞り込む作業が必要となるかもしれません。Silent Process Exit Monitoringの機能で自己終了したときのダンプ ファイルを取得することも可能ですが、正常終了時と区別できない状況の場合はやはり調査が困難です。不用意な例外処理がこのような状況を招くため、アプリケーションのエラー処理を設計・実装する際は十分に注意する必要があります。

不用意な例外処理について

アプリケーションのデバッグを困難にする不用意な例外処理については以下のような例が挙げられます。なお、これらはC#のコード例ですが、C/C++におけるtry/catchやWindowsの__try/__exceptブロックにおいても同様の問題が発生する可能性があります。また、.NETのAppDomain.UnhandledExceptionイベント、WindowsのSetUnhandledExceptionFilter関数などで未処理例外をキャッチしてログを記録したりアプリケーションを終了する場合も同様です。

例外の飲み込み

例外をキャッチして何もしない、またはエラーメッセージを表示しない場合、問題の原因を特定することが難しくなります。例えば以下の例ではDoBadThingメソッドで発生するアクセス違反例外を飲み込んでいますが、ヒープの破損など致命的な問題でアクセス違反に至っている場合は後続のDoSomethingメソッドでも同様にアクセス違反例外が発生する可能性があります。このような場合、ダンプファイルを取得して例外のスタックを確認しても問題が起きていたDoBadThingメソッドではなくDoSomethingメソッドで発生した例外により異常終了に至ってるように見える可能性があります。

[HandleProcessCorruptedStateExceptions]
public static int Main()
{
try
{
// アクセス違反例外が発生する可能性があるコード
DoBadThing();
}
catch (AccessViolationException e)
{
// 何もしない
}
DoSomething();
}

汎用的な例外キャッチ

すべての例外をcatch (Exception e)でキャッチすると、例外の原因を特定することが難しくなります。例えば以下の例では、ファイルが存在しない場合やファイルのアクセス権がない場合、メモリ不足で文字列が割り当てられなかった場合などいくつか例外が発生する状況がありますが表示されるエラーメッセージからは区別することができません。また、DoSomethingメソッド内でアプリケーションで処理を継続できないような問題が生じた場合も処理が継続するため、後続の処理でさらに別の問題が生じる可能性があります。

try
{
string content = File.ReadAllText("sometext.txt");
return DoSomething(content);
}
catch (Exception e)
{
Console.WriteLine("エラーが発生しました。");
return string.Empty;
}

対処方法としては、アプリケーション内で対処が可能な種類の例外に限定してキャッチを行い、他の例外はキャッチしないようにします。

try
{
string content = File.ReadAllText("sometext.txt");
return DoSomething(content);
}
catch (FileNotFoundException e)
{
Console.WriteLine("sometext.txt が見つかりませんでした。");
return defaultResult;
}
// 他の例外は処理せず呼び出し元に委ねる

例外チェーンを形成しない

例外チェーンは、例外を再スローする際に元の例外情報を保持して例外のスタックトレースを伝搬するために必要となります。これが適切に行われていない場合、例外のスタックトレースが伝搬されず根本の例外発生箇所を特定することが難しくなります。特によく見かける間違いとしては、以下のように例外をキャッチしてログを記録した後、例外オブジェクトをそのままスローしてしまうケースがあります。

public static void Main()
{
try {
// 例外が発生する可能性があるコード
DoSomething();
}
catch (Exception e)
{
logger.Error("some useful message.");
throw e;
}
}

このようにすると、イベントログやダンプファイルでコールスタックを確認した際にDoSomethingメソッドで発生した例外としてではなくMainメソッドで発生した例外に見えてしまいます。問題の原因はDoSomethingメソッドにありますが、これでは原因の特定が困難になったり、原因を誤認させてしまう可能性があります。

アプリケーション:ConsoleApp1.exe
フレームワークのバージョン:v4.0.30319
説明: ハンドルされない例外のため、プロセスが中止されました。
例外情報:System.NullReferenceException
場所 ConsoleApp1.Program.Main()

対処方法としては、元の例外情報を保持して例外を再スローします。例外クラスでは基本的に原因となった内部例外を渡すことができるコンストラクターのオーバーロードが用意されていますので、こちらを使用します。

public static void Main()
{
try {
// 例外が発生する可能性のあるコード
}
catch (Exception e)
{
// 例外を再スローし、例外チェーンを形成
logger.Error("some useful message.");
throw new Exception("some useful message.", e);
}
}

もしくは、メッセージなどの付加情報が必要なければ単にthrowのみを記述して例外を再スローする形でも構いません。

public static void Main()
{
try {
// 例外が発生する可能性のあるコード
}
catch (Exception e)
{
// 例外をそのまま再スロー
logger.Error("some useful message.");
throw;
}
}

こうするとイベントログやダンプファイルでコールスタックを確認した際に、例外が発生した元の箇所を特定することが可能になります。

アプリケーション:ConsoleApp1.exe
フレームワークのバージョン:v4.0.30319
説明: ハンドルされない例外のため、プロセスが中止されました。
例外情報:System.NullReferenceException
場所 ConsoleApp1.Program.DoSomething()
場所 ConsoleApp1.Program.Main()

例外情報:System.Exception
場所 ConsoleApp1.Program.Main()