LASSIC Media らしくメディア
例外処理の基礎|エラーハンドリングの考え方
例外処理とは
プログラムを動かしていると、ファイルが見つからない、通信が急に切れる、想定していない形式のデータが入力されるといった、開発時にすべてを予測しきれない事態にぶつかることは避けられません。こうした「想定外」が起きたときに、プログラムの実行をどう扱うかをあらかじめ取り決めておく仕組みが例外処理です。
例外処理が整っていないシステムでは、想定外の入力や通信断が起きた瞬間に処理が停止したり、内部のエラー内容がそのまま利用者の画面に表示されたりすることがあります。逆に例外処理を丁寧に設計しておくと、エラーが起きても影響範囲を限定し、利用者には分かりやすい案内を出しながら、開発側は原因を追いやすい記録を残せるようになるでしょう。
例えば、注文処理を行うシステムを考えると、在庫データベースへの接続が一瞬途切れる、決済代行サービスからの応答が遅れる、入力された数量が数値ではない文字列だったといった事態が起こり得るものです。これらを想定外の出来事として片付けてしまうと、利用者は原因の分からないまま処理が止まった画面を見ることになりかねません。設計段階から例外処理を組み込んでおけば、こうした事態が起きてもシステム全体を止めずに影響を局所化しやすくなります。
想定外の事態には、ネットワークの不調や外部サービスの障害のように利用者側では制御しきれない性質のものもあれば、想定していない入力値の組み合わせのように設計や実装を見直すことで防ぎやすくなる性質のものもあるでしょう。テストである程度は洗い出せるとしても、本番環境ではテストで再現しきれなかった条件に遭遇する場面が出てくるものです。両方の性質を踏まえて対応方針を分けておくと、原因の切り分けがしやすくなります。
本記事では、特定のプログラミング言語や製品に限定せず、例外処理という設計テーマの基礎的な考え方を整理するのが狙いです。プログラム実行中に生じた異常を検知し、適切な層で捕捉して対処するという視点は、言語やフレームワークが変わっても共通するものといえるでしょう。
例外とエラーの基本
例外(exception)とは、プログラムの実行中に発生した異常な状態を表すオブジェクトや情報のことです。「ファイルを開こうとしたが存在しない」「配列の範囲外を参照した」「通信がタイムアウトした」といった事象が起きると、多くの言語ではその内容を表す例外が生成され、呼び出し元に伝えられます。
多くの言語では、例外オブジェクトにエラーの種類を示す名前、原因を説明するメッセージ、発生箇所を追跡するための呼び出し履歴(スタックトレース)といった情報が含まれています。catchブロックでは、これらの情報をもとにどの例外が起きたのかを判別し、対応を分岐させるための手がかりとするものです。
例外の中には、通信のタイムアウトのように再試行によって回復が見込めるものもあれば、プログラムのロジック自体に誤りがあることを示すもののように、その場で回復させることが難しいものもあります。前者は自動的なリトライや代替経路への切り替えで対応できる場合が多く、後者は開発側が原因を修正しない限り、同じ状況を繰り返す可能性が高いといえるでしょう。
この例外を扱う基本的な仕組みが、try・catch・finallyという構造です。大まかな流れは、次のようになります。
try {
// 例外が起きるかもしれない処理を実行
} catch (error) {
// 例外を受け取り、対処する
} finally {
// 成功しても失敗しても行う後始末
}
tryブロックの中で処理を監視し、途中で異常が起きるとその場で処理を中断して、catchブロックへ制御が移る仕組みです。catchでは受け取った例外の内容を確認し、ログに記録する、代替の処理に切り替える、利用者向けの案内を出すといった対応を行います。finallyはtryやcatchの結果にかかわらず実行される部分で、ファイルのクローズや接続の解放など、後始末の処理を書く場所です。
自分のコードの中で異常を検知し、意図的に例外を発生させたい場合は、throwという仕組みを使います。呼び出し元に「ここで問題が起きた」と伝えるための送出であり、catchする側が想定していれば適切に受け止められます。
tryブロックの途中で処理を抜けるための命令を書いた場合でも、finallyに記述した内容は基本的に実行される仕組みになっているといえるでしょう。この性質を活かし、接続のクローズやロックの解放など、結果によらず行いたい処理をfinallyにまとめておく書き方が広く採用されています。
業務上意味のある単位で独自の例外を定義しておくと、catch側で「在庫不足」や「入力形式エラー」といった具体的な状況を判別しやすくなるものです。汎用的な例外だけに頼ると、発生した状況を後から特定するためにメッセージ文字列を都度確認するといった手間が生じやすくなります。
例外という仕組みが登場する以前は、戻り値としてエラーコードを返し、呼び出し元がその都度確認するという方法もよく使われていました。この方法では、確認を一つでも書き忘れると異常に気づけないまま処理が進んでしまう恐れがあります。例外の仕組みでは、対応するcatchが見つかるまで自動的に呼び出し元へ伝播していくため、確認漏れによって異常が握りつぶされる場面を減らしやすくなっているといえるでしょう。
例外処理の設計方針
例外処理で悩ましいのは、構文の書き方よりも「どこで捕まえるか」という設計です。発生した場所ですぐさまcatchしてしまうと、本来もっと上位の層で判断すべき内容まで握りつぶしてしまうことがあります。反対に、どの層でも捕まえないままにしておくと、例外がシステム全体に伝播し、処理全体が止まってしまうことにもなりかねません。
- 握りつぶしのリスク:catchした例外を何も記録せずに無視してしまうと、実際には問題が起きているのに正常に動いているように見えてしまいます。原因調査の手がかりも失われ、後になって別の不具合として表面化しやすくなるものです。
- リトライの判断:通信タイムアウトのように一時的な要因で起きる例外は再試行で解消することがありますが、入力値の誤りのように再試行しても変わらない例外もあります。どちらに該当するかを区別しておく必要があります。
- 利用者への見せ方:内部のスタックトレースやシステム構成をそのまま画面に出すのは避け、利用者には「時間をおいて再度お試しください」といった分かりやすい案内にとどめるのが一般的です。
- ログの重要性:利用者向けの表示を簡潔にする分、開発側が原因を追えるよう、発生日時・処理内容・例外の詳細をログに残しておくことが欠かせません。
リトライを行う場合は、同じ処理を複数回実行しても結果が変わらない設計になっているかどうかも合わせて確認しておく必要があります。決済のように一度きりであるべき処理を無自覚に再実行してしまうと、二重処理という別の問題につながりかねないものです。加えて、例外の発生頻度や種類を監視できる仕組みを用意しておくと、障害の予兆を早期につかみやすくなります。
近年の多くのフレームワークやアプリケーション基盤には、個々の処理で拾いきれなかった例外を最終的に受け止める、共通のエラーハンドリングの仕組みが用意されています。個別の処理でこまごまとエラー表示を書かなくても、最終防御ラインとして共通の仕組みに任せておける設計は、実装の抜け漏れを減らすうえで有効な考え方です。
多くのシステムは、画面に近い層、業務ロジックを扱う層、データベースなど外部と接続する層といった複数の層で構成されています。例外をどの層で捕まえるかは、この層構造を踏まえて決めることが多く、外部接続に近い層で発生した例外をそのまま画面に近い層まで生の形で伝えてしまうと、利用者向けの表示が乱れる原因になりがちです。業務ロジックの層で一度受け止め、意味のある形に変換してから上位に伝える設計がよく用いられます。
この設計を怠ると、障害発生時に「何が起きたか分からない」状態に陥りやすく、復旧までの時間が延びる原因になりがちです。
良い例外処理・避けたい例外処理
例外処理には、後から見直したときに差が出やすいポイントがいくつかあります。代表的な観点を対比すると、次の表のようになります。
| 観点 | 避けたい書き方 | 好ましい書き方 |
|---|---|---|
| 例外を受け取ったときの対応 | catchした例外を何もせず握りつぶす | ログに記録したうえで、必要に応じて呼び出し元に再送出する |
| 捕捉する範囲 | catch(Exception)のようにすべての例外をひとまとめに受け止める | 想定される例外を型ごとに分けて捕捉し、それぞれに合った対応を行う |
| エラーメッセージ | 内部のパス名やスタックトレースをそのまま利用者に表示する | 利用者向けの案内と、開発者向けの詳細ログを分けて出力する |
| 再送出の判断 | その場で握りつぶしたまま処理を続ける | 対応できない例外はログに記録したうえで、呼び出し元に再送出する |
catch(Exception)のようにすべてを一括りに受け止める書き方は、メモリ不足や設定ミスなど、本来はアプリケーションの再起動や運用側の対応が必要な重大な異常まで、同じ経路で処理してしまう可能性があるでしょう。結果として、深刻な問題が軽微なエラーと同じ扱いになり、気づかれないまま放置されるリスクが生じやすくなります。
エラーメッセージを利用者向けと開発者向けに分けておくと、画面には「時間をおいて再度お試しください」といった簡潔な案内だけを表示し、内部のパス名や処理の詳細はログにのみ記録するという運用がしやすくなるものです。この分離ができていないと、意図せず内部構成の情報を外部に晒してしまうことにもつながりかねません。
すべての例外を一つのcatchでまとめて受け止める書き方は、コードこそ短くなるものの、本来個別に対応すべき例外まで同じ扱いになってしまうものです。想定した種類ごとに捕捉先を分けておくほうが、後からの保守はしやすくなります。catchした例外がその場で対応しきれない場合、無理に握りつぶさず、ログに記録したうえで呼び出し元に再送出するという選択肢もあります。呼び出し元がより広い文脈を把握していれば、リトライや利用者への通知など、その場では判断できなかった対応を行える場合があるでしょう。
制御の流れを図にすると、次のようになります。
実務での関わり
堅牢なシステムかどうかは、正常系の機能だけでなく、例外処理の質によって左右される部分が大きいといえます。想定外の入力や外部サービスの不調が起きたときにどう振る舞うかは、実際に運用が始まってから差として表れやすいところです。
発注やレビューの場面では、次のような観点を確認しておくと、後々のトラブルを減らしやすくなります。
- エラー発生時の挙動が仕様として明文化されているか(画面表示、リトライの有無、通知先など)
- 例外の発生状況がログとして残り、後から追跡できる形になっているか
- 利用者に内部情報がそのまま見える、不親切なエラー画面になっていないか
- 一部の機能で例外が起きた際に、システム全体が連鎖して止まる構成になっていないか
正常系のテストだけでなく、意図的に例外を発生させた場合の挙動を確認するテストを用意しておくと、リリース後に想定外の挙動が発覚するリスクを減らせます。障害を想定した訓練やテストケースの設計段階から、例外処理の経路も対象に含めておくことが望ましいでしょう。
例外処理の不備は、リリース直後よりも、利用者数が増えてアクセスが集中する時期や、外部サービス側で障害が起きたタイミングで表面化しやすい傾向があります。あらかじめ余裕を持って設計しておくことで、想定外の事態が起きたときの対応工数を抑えられるでしょう。
取引先の担当者が業務で使うシステムの場合、内部エラーがそのまま表示された画面は、ツールへの信頼感を損ねる要因になりやすいものです。エラー画面ひとつであっても、利用者から見れば発注したシステム全体の品質を判断する材料になり得ます。
要件定義や設計のドキュメントに、代表的な異常系のシナリオと期待する挙動を書き添えておくと、開発者間で認識をそろえやすくなります。実装後に仕様が定まっていない部分が見つかると、手戻りが発生しやすくなる点にも注意が必要です。
これらは開発の初期段階で仕様に落とし込んでおくほうが、後から手を入れるより負担が小さく済みます。設計レビューの段階で例外処理の方針を確認しておくことは、リリース後の運用コストを抑えることにもつながるでしょう。
まとめ
例外処理は、プログラム実行中に起こる想定外の事態を検知し、適切な層で対処するための仕組みです。try・catch・finallyという基本構造を押さえたうえで、どこで捕まえるか、握りつぶさずログに残すか、利用者にどう見せるかといった設計方針を決めておくことが重要になります。すべての例外を一括りに捕まえるのではなく、想定した種類ごとに対応を分け、内部情報と利用者向けの案内を区別しておくと、保守や障害対応がしやすくなります。
発注やレビューの場面でも、例外処理の仕様化とログの有無を確認しておくとよいでしょう。
相談するメリット
例外処理の設計は、後から見直そうとすると影響範囲が広がりやすい部分です。「エラー時にどう振る舞うべきか整理しきれていない」「ログはあるが原因追跡に使える形になっていない」といったお悩みがあれば、LASSICにご相談ください。
LASSICでは、ニアショア開発体制を活かし、既存システムの例外処理・エラーハンドリングの見直しから、新規開発における設計方針の策定まで、堅牢なシステムづくりを支援しています。受託開発の一環として、コードレビューや設計ドキュメントの整備もあわせて対応可能です。
よくある質問
例外は握りつぶしてはいけないのでしょうか。
catchした例外を何も処理せず放置すると、実際には問題が起きているのに気づけなくなります。最低限ログに記録し、必要であれば呼び出し元に再送出するなど、何らかの形で対処を残しておくことが望ましいです。
finallyは何のためにあるのですか。
tryやcatchの結果にかかわらず、ファイルのクローズや接続の解放といった後始末の処理を行うためのブロックです。正常終了・例外発生のどちらの場合も実行されます。
すべての例外をcatch(Exception)でまとめて受け止めるのは良くないのでしょうか。
コードは簡潔になりますが、本来個別に対応すべき例外まで同じ扱いになり、原因の切り分けが難しくなります。想定される例外を型ごとに分けて捕捉するほうが、後の保守がしやすくなります。
利用者にエラーメッセージをどこまで見せればよいですか。
内部のパス名やスタックトレースをそのまま見せるのは避け、利用者向けには分かりやすい案内にとどめ、詳細情報は開発者向けのログに残す形が一般的です。
著者:テレリモ総研編集部 鈴木 亮佑