エラーハンドリングについて
こんにちは!株式会社コミュニティオで主にサーバーサイドの開発を担当しているエンジニアの竹田です。今回のテーマはエラーハンドリングです。
エラーハンドリングの基本
エラーハンドリングはエラーの検出・エラーの伝達・エラーへの対処の一連の流れを指す。正しいプログラムがリカバリー可能な何かしらの問題(エラー)に遭遇したときに、その問題を解決するための方法を提供することがエラーハンドリングの目的である。
独立行政法人情報処理推進機構 セキュリティセンター, セキュア・プログラミング講座2007年版 C/C++言語編 第6章 2.エラーハンドリング, https://www.ipa.go.jp/archive/security/vuln/programming/cc/chapter6/cc6-2.html
エラーハンドリングの重要性
エラーハンドリングを適切に行うことで、以下のようなメリットが得られる。
システムの安定性向上
エラーハンドリングを適切に行うことで予期しないエラーが発生した際にもシステムがクラッシュするのを防ぎ、安定した動作を維持できる。
ユーザー体験の向上
エラーが発生した際にユーザーに適切なメッセージを表示することで、ユーザーが何が起こったのか理解しやすくなり、混乱を避けられる。
デバッグとメンテナンスの容易化
エラーハンドリングを適切に行うことで、エラーの原因を特定しやすくなり、デバッグやメンテナンスが容易になる。
セキュリティの向上
エラーハンドリングを適切に行うことで、エラー情報が外部に漏れるのを防ぎ、システムのセキュリティを向上させる。
データの保護
エラーハンドリングを適切に行うことで、データの不整合や損失を防ぎ、データの整合性を保つ。
エラ-の伝達方式(Exception or not Exception)
エラーの伝達には例外(Exception)と例外以外の仕組みを利用した2つの方式がある。
例外を利用する方式は、エラーが発生した際に例外をスロー(伝搬)し例外をキャッチしてエラーを処理(対処)する。エラー処理が正常系の処理と分離されるため、コードがシンプルになるメリットがある。一方、処理の流れが暗黙的になりエラー処理を強制できないデメリットがある。
関数型プログラミングの普及に伴いパターンマッチングやモナド(Either)を利用したエラー処理が一般的になり、従来であれば例外を利用する場面でも例外以外の方式を利用することが増えている。この方式ではエラー処理が明示的になりエラー処理を強制できるメリットがある。一方、言語サポートがない場合、正常系とエラー処理のコードが混在しコードが複雑になるデメリットがある。
例外(Exception)を利用するべき場面
予期しないエラー
通常のプログラムの流れでは発生しない予期しないエラー(例: ファイルの読み込み失敗、ネットワークエラーなど)。
リソースの不足
メモリ不足やディスクスペース不足などリソースが不足している場合。
外部システムのエラー
外部APIやデータベースの接続エラーなど外部システムとの通信に失敗した場合。
重大なエラー
プログラムの継続が不可能な重大なエラー(例: データの整合性が保てない場合)。
例外を利用するべきでない場面
通常の制御フロー
例外を通常の制御フローの一部として使用するのは避けるべき(例: ループの終了条件として例外を使用する)。
バリデーションエラー
ユーザー入力のバリデーションエラーなど、予測可能で通常の処理の一部として扱うべきエラー。
パフォーマンスが重要な部分
例外処理はオーバーヘッドが大きいためパフォーマンスが重要な部分では避けるべき。
簡単に回避可能なエラー
簡単に回避可能なエラー(例: 配列の範囲外アクセスを防ぐための事前チェック)では、例外を使用せずに事前にチェックする。
プログラム構成とエラーハンドリング
プログラムはツリー構造であり一般的にツリーのルートは制御の入口でリーフは外部システム(DBや3rdパーティー・サーバー)への呼び出しとなる。エラーはリーフで発生することが多い。
フロントエンドのコード構成。
エラーへの対処
エラー伝達を止めるためにはエラーを対処する必要がある。対処方法は3つ。
- Abort: Report & Terminate
- Retry
- Ignore: Report & Continue
Abort: Report & Terminate
エラーを検出したらエラーを報告してプログラムを終了する。Abort: Report & Terminateはプログラムツリーのルート近くで行う。
プログラムツリーのルートはリーフと比較してより広いコンテキストを持っているためエラーを適切に報告できる。例えばDB接続エラーが発生した場合、コントローラーであれば誰がリクエストしたのかの情報を持っている。DBアクセス近くのコードはリクエストしたユーザー情報を持っていないことが多い。
不正な状態のままプログラムが実行されないようにプログラムを終了することでシステムの健全性を保護する。例えば必要な環境変数が設定されていない場合、プログラムは正しく動作しないためエラーを報告してプログラムを終了する。
Retry
エラーを検出したら処理を再度試行する。Retryはプログラムツリーのリーフ近くで行う。
失敗した箇所の近くのコードは失敗した処理の実装の詳細・理由を知っている。外部API呼び出しであればAPI予備だ周辺のコードはレスポンスコードやエラーメッセージを取得して適切に解釈できる。プログラムのルート近くでリトライしようとすると、リトライが適切かどうか判断できない。ルートでリトライの判断をさせようとするとルートに実装の詳細を持たせる必要がある。
リトライにはCircuit Breakerパターンやバックオフアルゴリズムなど多くの設計パターンがあるが、安全にリトライを行うために処理の冪等性が必要な場合が多くリトライができる部分は限られている。 例えばSendGridでメール送信エンドポイントを実行後、ネットワークエラーが発生した場合、単純にリトライするとメールが重複して送信される可能性がある。この場合リトライを行う前にSendGridのエンドポイントが冪等性を持っているかどうかを確認する必要がある。
Ignore: Report & Continue
エラーを検出したらエラーを報告して処理を続行する。
Abort: Report & Terminateと同様にエラーの報告はプログラムツリーのルート近くで行う。リーフでエラー報告を行おうとするとエラー報告のためのコンテキストをリーフまで伝達する必要がある。Teams Appサーバーでは関数パラメータの多くは実際の処理に必要な情報ではなくエラー報告のための情報であるためコードの見通しが悪くリファクタリングが困難になっている。
処理が逐次実行される、またはオプショナルな場合に適切。例えばTeams通知を100ユーザーへ行う場合、1ユーザーごとにTeamsのエンドポイントを実行する。エラーが発生した場合、そのユーザーには通知が行われないが他のユーザーには通知が行われる。
Ignore: Report & Continueする場合、エラー発生時にシステムの状態が変更されている場合、システムの状態の整合性が保たれるように注意が必要。REST APIサーバーでは変更対象の状態はリクエスト毎にクリアされることが多いためエラー発生時にシステム状態の整合性が崩れる可能性は低い。例えばリクエストの処理中にDB接続が失敗しても、エラーを記録してリクエストを終了すればシステムの状態は変更されない。
実践的なエラーハンドリング
実際のプロジェクトでのエラーハンドリングの事例紹介
TeamSuiteサーバーのプロジェクトではエラーハンドリングを以下のように行っている。
環境変数の未設定エラーはAbort: Report & Terminateしている。環境変数が未設定の場合、例外を投げてプログラムを終了する。例外には環境変数名を含めてデバッグしやすいようにしている。
リクエストの入力値のバリデーションエラーはIgnore: Report & Continueしている。バリデーションエラーが発生した場合、エラーメッセージを返却してリクエストを終了する。バリデーションはシステム状態の変更前に行われるため、エラーが発生してもシステム状態の整合性が崩れることはない。
ブラウザやメールへの通知はIgnore: Report & Continueで処理している。ユーザーへの通知の様に複数の逐次処理の場合、エラー報告の頻度を抑えるためにエラーをキャッチしてリストに追加し、全処理が終了した後にエラーを一括で報告する。Sentryなどエラー報告にネットワーク通信が必要な場合、ループ中にエラー報告を行うと実際の処理の性能に影響を及ぼす可能性があるので注意。
グループ情報の更新処理はRetryしている。グループ情報の更新時にそのグループが存在しない場合、更新に失敗する。グループ情報の更新は後勝ちという冪等性のある仕様のため、グループが存在しない場合はグループを作成してから更新を行う。
コードレビューでのエラーハンドリングのチェックポイント
コードレビューでエラーハンドリングをチェックする際には以下のポイントに注意する。
- エラーが発生する場所(外部システムへの依存)はプログラムのリーフに近いか。
- エラー伝達の方法は適切か。例外を利用するべきか、例外以外の型を利用するべきかを適切に選択しているか
- Abort: Report & Terminate, Retry, Ignore: Report & Continueのなかから適切なエラーへの対処方法を選択しているか
- エラーの対処を行う場所は適切か。リーフの部分でtry-catch&ログ出力してエラーを握りつぶしていないか。ルート部分でリトライしていないか。
議論
ギフトのエラーハンドリングについて
お客様「竹田」が店舗「Communitioコンビニ」でIn Houseギフト「おつまみ」を利用する。店舗コード「1000」を入力、「使用する」ボタンを押下。
ギフトを利用するエンドポイント実行時にネットワークエラーが発生した。エラーをどのようにハンドリングするか?
- エラー対処方法はAbort、Retry、Ignoreのどれを選択するか。
- ユーザーにはどのようにエラーを表示するか。
- ネットワークエラーのためサーバーサイドにリクエストが到達したかしていないか、到達していた場合サーバー側で処理が成功したかしていないか不明。リクエストが未到達の場合、サーバー側にリクエストが到達して処理が成功した場合、サーバー側にリクエストが到達して処理が失敗した場合のエラーハンドリングがユーザーにどのような影響を与えるか。
- エラー報告にはどのような情報を含めるか。
- エラー報告はどのように行うのが適切か。ネットワークエラーが起きているところでエラー報告を行うとエラー報告自体が失敗する可能性がある。
社内ポイントシステムの決済エラーハンドリングについて
お客様「竹田」が社内コンビニで社内ポイントで商品を購入する。100ポイントの商品を購入しようとして「支払い」ボタンを押下。
社内ポイントの支払いエンドポイント実行時にネットワークエラーが発生した。多重決済を防ぐにはどのようなシステム設計を行えばよいか?
最後に
エラーハンドリングはエラーの検出・エラーの伝達・エラーへの対処の一連の流れを指す。エラーハンドリングを適切に行うことでシステムの安定性向上、ユーザー体験の向上、デバッグとメンテナンスの容易化、セキュリティの向上、データの保護が期待できる。
プログラムはツリー構造でエラーはリーフで発生することが多い。エラーの対処方法はAbort: Report & Terminate, Retry, Ignore: Report & Continueの3つがある。対処は適切な場所で行う必要がある。
プログラム内で全てのエラーに対処することは難しいため、エラーハンドリングの設計はエラーの発生確率や影響度合いを考慮して行う必要がある。必要であればシステム外でエラーに対応することも検討する。システム外での対応に必要な情報はエラー報告に含める。
Discussion