📄

例外処理について

に公開

例外処理とは

プログラムを実行していると、思わぬ問題が発生することがある。たとえば、存在しないファイルを読み込もうとしたり、0 で割り算をしようとした場合などがそれにあたる。

このような予期しない事態が起こったときに、プログラムが異常終了しないよう、安全に処理を続けたり終了したりするための仕組みが「例外処理」である。

例外とエラーについて

「エラー」とは、プログラムに何らかの問題が発生したことを表す広い概念である。一方、「例外(Exception)」は、プログラムの実行中に発生する特別なエラーであり、プログラムの処理の流れを中断させて、別の処理に切り替えるための信号のようなものである。

  • エラー:コンパイルエラーや構文ミスなど、コードを書く段階で検出されるもの
  • 例外:実行時に発生する予測困難な問題(例:ファイルが見つからない、0 での除算)

主な発生事例

以下は、例外が発生しやすい代表的なケースである:

  • 存在しないファイルを開こうとしたとき(FileNotFoundException
  • ネットワークエラーによって通信が失敗したとき(HttpRequestException
  • 配列の範囲外にアクセスしようとしたとき(IndexOutOfRangeException
  • 0 で割り算しようとしたとき(DivideByZeroException
  • ユーザーの入力が数値でないとき(FormatException

例外処理をするべき箇所

例外処理をすべき場所は、外部環境に依存している処理や、ユーザー入力に応じた処理など、失敗する可能性がある部分である。

たとえば、以下のような処理では例外を考慮する必要がある:

  • ファイルの読み書き
  • ネットワーク通信(API 呼び出しなど)
  • データベースとのやりとり
  • ユーザー入力の解析や数値変換
  • 配列・リスト・辞書などのコレクション操作

これらの処理は、常に成功するとは限らないため、例外処理によって適切に対処しておくことが重要である。

例外処理の書き方

C#における基本的な例外処理は、try-catch-finally 構文を使用する。

try
{
    // 例外が発生する可能性のある処理
    int result = 10 / 0;
}
catch (DivideByZeroException ex)
{
    // 例外が発生した場合の処理
    Console.WriteLine("0で割り算しようとしました。");
}
finally
{
    // 成否にかかわらず最後に必ず実行される処理
    Console.WriteLine("処理が終了しました。");
}
  • try:例外が発生するかもしれない処理を記述する
  • catch:例外が発生したときの対応を記述する
  • finally:例外の有無にかかわらず、最後に実行される処理を記述する(省略可能)

例外の伝播

例外が発生したとき、それが catch によって処理されない場合、その例外は呼び出し元のメソッドへと伝わっていく。この流れを「例外の伝播(でんぱ)」と呼ぶ。

以下のコードは、C メソッドで発生した例外が B、さらに A へと伝播していく様子を示している。

void A()
{
    try
    {
        B();
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Aで例外をキャッチ: {ex.Message}");
    }
}

void B()
{
    C();
}

void C()
{
    throw new Exception("Cでエラーが発生しました");
}

このように、例外は処理されるまでスタック(呼び出し階層)をさかのぼって伝播していく。いずれかの場所でキャッチされなければ、最終的にプログラムは強制終了する

再スロー(rethrow)の活用

catch ブロック内で例外を補足したあと、さらにその例外を上位に投げ直すことを「再スロー」と呼ぶ。

正しい再スローの例

try
{
    DoSomething();
}
catch (Exception ex)
{
    Console.WriteLine("ログ出力: " + ex.Message);
    throw; // 元の例外情報を保持して再スロー
}

このように throw; を使用すれば、例外が発生した元の位置(スタックトレース)を維持したまま、上位に伝えることができる。

よくある誤り:throw ex; によるスタックトレースの破壊

catch (Exception ex)
{
    Console.WriteLine("ログ出力: " + ex.Message);
    throw ex; // NG:スタックトレースがこの行から始まってしまう
}

このように throw ex; としてしまうと、例外の発生場所が現在の catch ブロックであるかのように扱われてしまうため、本来の発生箇所が失われてしまう。これは、デバッグやログ解析に悪影響を与える。

スロー方法 スタックトレース保持 説明
throw; 保持される 元の例外発生箇所がそのまま記録される
throw ex; 上書きされる 発生元が catch ブロックに書き換わる

再スローの使いどころと注意点

再スローは次のような場面で有効である:

  • 下位層でログ出力だけ行い、処理の判断は上位層に委ねたいとき
  • 特定の例外は処理し、それ以外はそのまま伝播させたいとき
  • ライブラリ側で例外をラップせず、そのままアプリケーション側に投げたいとき

ただし、不要な再スローや、すべての例外を再スローするような乱用は避けるべきである。**再スローは「責任の委譲」**であることを意識し、処理の責任範囲を明確にした設計を行うべきである。

例外処理のアンチパターン

例外処理は便利だが、使い方を誤るとバグの温床になる。ここでは、よくあるアンチパターンを紹介する。

エラーを握りつぶす(ログもスローもなし)

try
{
    ProcessData();
}
catch (Exception)
{
    // 何もせず無視するしたり、真偽値のみ返したいする
}

これは「エラーの握りつぶし」と呼ばれる危険なパターンである。エラーが発生しているにもかかわらず、それに対する対応が行われないため、バグに気づくきっかけすら失われる

最低限、ログ出力や通知などで状況を記録すべきである。

try
{
    ProcessData();
}
catch (Exception ex)
{
    Console.WriteLine($"エラーが発生しました: {ex.Message}");
}

また、必要であれば例外を再スローして、上位に通知することも考慮する。
全体でエラーをハンドリングするとなおよし。

catch (Exception ex)
{
    Console.WriteLine($"致命的エラー: {ex.Message}");
    throw;
}

何でもかんでも Exception でまとめてキャッチする

catch (Exception ex)
{
    Console.WriteLine("何かのエラーが発生しました。");
}

このように、すべての例外を Exception 一つでまとめてキャッチすると、具体的な原因が分かりにくくなり、適切な対応ができなくなる

想定される例外は個別にキャッチすべきである。

catch (FileNotFoundException ex)
{
    Console.WriteLine("ファイルが見つかりません。");
}
catch (FormatException ex)
{
    Console.WriteLine("入力の形式が不正です。");
}

例外に頼りすぎる

本来であれば事前に条件をチェックすべきところで、例外処理に頼ると、意図が不明瞭になり、パフォーマンスにも悪影響を与える。

try
{
    var item = list[10]; // インデックス範囲外
}
catch (ArgumentOutOfRangeException)
{
    // 何もしない
}

このような場合は、以下のように条件分岐で対処するのが適切である。

if (list.Count > 10)
{
    var item = list[10];
}

おわりに

初心者のうちは「とりあえず try-catch を書く」ような使い方になりがちだが、
例外の仕組みと役割をしっかり理解した上で、「どこで・なにを・どうやって処理するのか」 を明確に意識することが重要である。

GitHubで編集を提案

Discussion