プログラミングで分かりづらいテーマの一つが例外処理だと思います。例外処理は正しく設計しないと問題が起きたときにアプリケーションの障害につながってしまいます。一方で例外処理を学習だけで理解するのは難しく、実務での経験をとおして身につける必要があると思っています。
私も実務で例外処理を経験するまでは「なんとなく知っている」レベルでした。実務でレビューを受けたりしながら例外処理について学びました。この経験をもとに、この記事では例外処理とはなにか、目的や考え方について書いていきます。
この記事は、プログラミングを学習中の方、実務経験が浅いエンジニアの方を主な読者対象としています。私がメインで使っているプログラミング言語はRubyですが、言語を問わない汎用的な内容になっていると思います。
テクニカルライター。元エンジニア。共著で「現場で使えるRuby on Rails 5」を書きました。プログラミング教室を作るのが目標です。
プログラミングにおける例外処理とは
まず例外について整理しておきます。例外とは、「想定していないことが起きたときに、問題の解決=回復を期待して呼び出し元にそれを伝えること」をいいます。この例外を処理することが例外処理になります。
呼び出し元に例外を投げて、そこで解決されない場合は次々と上位の呼び出し元に伝搬します。例外処理の全体をとおしても問題が解決されないとき、アプリケーションに障害が起こります。
どんなときに例外が起こるか
そもそも、どんなときに例外は起こるのでしょうか。たとえば、次のようなケースで起こったりします。
- API連携など、結合している外部のアプリケーションに問題があるとき
- ゼロでの除算など、許されない演算を行ったとき
- ユーザーの入力にミスがあったとき
- ライブラリにバグがあったり、アップデートにより互換性が失われたとき
- ハードウェアが故障したとき
例外が起こる例
上で例外が起こるケースをいくつか見ましたが、具体的な例を見てみます。たとえば、アプリケーションで処理をしようとしているファイルがないときに例外処理が起こります。
C++やJava、JavaScriptやRubyなど、言語によっては例外処理をする機能を組み込みで提供しています。たとえばJavaは、文法上のミスがないなど、コンパイルさえできれば実行できます。ただ、コンパイル時に想定していなかったことが、実行段階で起こったりします。つまり例外が起こります。
ファイルの処理において、コンパイルの段階ではファイルの存在確認をしないので、実行時に例外が起きることがあります。このケースでは、ファイルがないときの例外処理をしないと、アプリケーションに障害が起きてしまいます。
この障害を防ぐ方法としては、たとえば画面上でユーザーにファイルの存在確認を促すような例外処理を行うことで、例外から回復することができます。
なぜ例外処理をするのか
例外処理は、例外に対応してアプリケーションを回復するために行います。例外処理を行わないと、アプリケーションに障害が起こってしまいます。これはユーザー体験の悪化につながります。ビジネスの種類によっては、ユーザーに実質的な損害を与えることになります。データの破損も起こりうるでしょう。受託開発においては、内容によっては瑕疵に相当するかもしれません。
例外が起きたときにアプリケーションを回復するのが例外処理の役割です。とても重要だといえます。
例外処理における用語の定義
例外処理の考え方に入る前に、次の用語について押さえておくことが大切なので、まず説明しておきます。
エラーと例外の違い
例外という文脈で似ている言葉として「エラー」があります。中には混用しているケースも見られます。両者の明確な定義はありませんが、次のように分類する場合があります。
用語 | 説明 |
---|---|
エラー | アプリケーションのユーザーが解決できる問題 |
例外 | アプリケーションの開発者が解決する必要のある問題 |
チェック例外と非チェック例外
また、Javaなど言語によっては呼び出し元に例外処理を強要する機能をもつものがあります。これについて、「チェック例外」と「非チェック例外」という考え方があります。
用語 | 説明 | 例 |
---|---|---|
チェック例外 | 例外処理が必要な例外 | ファイル読み込みなど |
非チェック例外 | 例外処理を行う必要がない例外 | メモリの枯渇など |
例外処理の考え方・設計指針
それでは、実際にどうやって例外処理を行えばいいのでしょうか。基本的な考え方は、「例外処理は回復するか投げっぱなしにするかのどちらか」で対応するとよいです。ここでは例外処理をすべきところ、逆にすべきでないところの例を書いていきます。
どこで例外処理をすべきか
まず例外処理をすべきところについて書きます。例外処理は、呼び出し先で起こりうる例外を回復できるところで行います。たとえば次のようなところです。
- APIにリクエストを送っているところ
- ファイルの読み込みを行なっているところ
- ユーザーからの入力を処理しているところ
このとき、回復可能な例外をできるだけ狭い範囲でキャッチします。たとえばExceptionではなくIOExceptionのような下位の例外を処理します。また、例外処理をしたときはクリーンナップ処理も行います。たとえばファイルのクローズ処理などです。
ほかにも、フレームワークの例外処理機構を使用することを検討します。たとえばRuby on Railsにはrescue_fromというメソッドがあります。これはコントローラのトップレベルで例外処理を定義できます。
どこで例外処理をすべきでないか
上で例外処理をすべきところについて書きましたが、今度は逆に例外処理をすべきではないところについて整理します。これについて、呼び出し先で起こりうる例外を回復できないところでは例外処理をすべきではありません。
例外が起こりうる呼び出し先があるからといって、直近の呼び出し元で例外処理を必ずしなければならないという訳ではありません。上位の呼び出し元に任せることも、大切な考え方のひとつといえます。たとえば、次のようなケースでは例外処理をすべきではありません。
- 例外をなかったことにすること。「例外を握りつぶす」といったりします。これをすると上位の呼び出し元が例外を検知できず、適切な回復処理を行えなくなります
- ログに書いて例外を再送出すること。上位の呼び出し元でも、同じ例外を元にしたログが書かれているはずなので、ログが二重になります。これは調査のときに混乱の元になってしまいます
どこで例外を投げるべきか
以上で、例外処理について書きました。では、例外を投げる側については、どういったときに投げればいいのでしょうか。この基本的な考え方としては、「呼び出しもとは呼び出す条件を満たしたけど、求められる処理を達成できなかったとき」に投げればよいです。このとき、次の二つのことを考慮します。
- 呼び出し元に例外処理を強要しないこと
- 例外クラスを定義するということは、呼び出し元で回復処理を期待することを意味する。そうでなければ標準例外クラスを使い、クラス定義はしない
まとめ
例外処理は、アプリケーションの障害を防ぐために重要な処理のひとつです。例外処理はただ行えばいいものではありません。ログだけ書いて握りつぶすような処理は、逆に問題を引き起こすことにつながります。
例外処理では可能な限り回復し、できなければ投げっぱなしにして上位の呼び出し元に任せる、といった設計指針でよいのかな、と思います。