プログラムを書いていると必ずエラーハンドリングを行うが、チームで開発を行っていると既存コードを考えなしに流用したり、個々人によって扱い方がバラバラだった
その結果、エラーが握りつぶされたり、不要なログが出力されていたりとシステムの健全な運用が難しい状態になってしまった
エラーハンドリングに関してネットの情報を探ってみても、用語が難しかったり複雑だったので、正しいエラーハンドリングとは何か自分なりにまとめてみた
正しいエラーハンドリングについて
エラーとは正常ではない動作をすることを表し、適切な対応を必要とする
適切な対応とは、①エラー内容に応じて安全な処理を行う②適切な人に適切なエラー内容を通知することである
①エラー内容に応じて安全な処理を行う
安全な処理とはエラーが起きた時に誤作動など起こさないための処理である
処理の方法は大きく分けて2つに分けられる
- 処理を中断する
- 処理を継続する
a. 処理を中断する
エラーが発生すると後続の処理を行うことができない or 意図しない動作をする場合などは処理を中断する
// エラーが起きた時に処理を中断するケースn, err := strconv.Atoi("10歳")if err != nil { // エラーが発生した場合、エラー内容を返して処理を中断 return herror.Wrap(err, "文字を数字に変換するのに失敗")}// nを使った処理・・・
b. 処理を継続する
エラーが起きても値を無視して良い場合や、デフォルト値を利用することで処理として問題ない場合、例外処理を行い処理を継続する
// エラーが起きた時に処理を継続するケースn, err := strconv.Atoi("10歳")if err != nil { // エラーが発生した場合はデフォルト値を利用して処理を継続 n = defaultNumber}// nを使った処理・・・
②適切な人に適切なエラー内容を通知
エラーというのは意図しない挙動なので、適切な人に適切な内容を通知する必要がある
どういうケースが考えられるかはエラーが発生する状況に依存するので、完璧に定義するのは難しいが、考えられそうなケースを例としていくつか挙げる
エラー内容 | 通知先 | 通知内容 |
---|---|---|
a. 入力値エラー | 入力元(+開発者) | エラー内容&正しいアクションなど |
b. アプリ内部エラー | 開発者(+当事者) | エラー内容&入力値&スタックトレースなど |
a-1. 入力値エラー→入力元に正しいアクションを含めたエラー内容を伝える
// 不正な入力がされたケースn, err := strconv.Atoi(age) // ex. age := "10歳"if err != nil { // エラーが発生した場合、エラー内容を返して処理を中断 return herror.Wrapf(err, "年齢の変換に失敗しました。数字のみ入力可能です。 %s", age) // エラー内容をフロントに返したくない場合 // return herror.New("年齢の変換に失敗しました。数字のみ入力可能です。 %s", age)}
a-2. 入力値エラー→入力元&開発者に同じエラー内容を伝達
// 不正な入力がされたケースn, err := strconv.Atoi(age) // ex. age := "10歳"if err != nil { // エラー内容を開発向けにログに残し、ユーザーにもエラーを伝える msg := fmt.Sprintf("年齢の変換に失敗しました。数字のみ入力可能です。 %s", age) logger.Error(msg, zap.Error(err)) return herror.Wrap(err, msg)}
a-3. 入力値エラー→入力元&開発者にエラー内容を分けて伝達
エラー内容によっては、入力元と開発者への伝達内容を分けた方が良い場合がある
// 入力されたIDのユーザーが見つからないケースuser, err := login(id, pass)if err != nil { // セキュリティ観点で具体的なエラー内容や入力値を開発者向けにログとして残し、ユーザーにはエラーのみ伝える logger.Error("ユーザーが見つかりませんでした", zap.String("id", id) zap.Error(err)) return herror.New("ログインに失敗しました")}
b-1. アプリ内部エラー → 開発者に入力値を含め通知
// DBからデータ取得に失敗するケースticket, err := db.TicketByID(t.DB, req.ID)if err != nil { msg := "find ticket error by ticket id" t.logger.Error(msg, zap.Int64("id", req.ID), zap.Error(err)) // エラー内容と入力パラメータを通知 return nil, herror.Wrap(err, msg)}
b-2. アプリ内部エラー→開発者+当事者に通知
// クローリングメディアのログインに失敗する場合session, err := yahoo.login(id, pass)if err != nil { msg := "yahooのログインに失敗しました" slack.Send(msg, err) // セールスなど正しいID・PWを知ってる人に再入力を促す logger.Error(msg, zap.String("id",id), zap.Error(err)) // 原因を把握するために詳細な状況をログとして残す return herror.Wrap(err, msg) // SQSで再処理を可能にするためエラーを返す}
通知方法について
現在通知方法としてロギングを使っているが、ログレベルの定義を揃えたい
レベル | 概要 | 説明 | 出力先 | 運用時(prd)の対応 |
---|---|---|---|---|
Error | エラー | 予期しないエラー | コンソール(slack) | 即時対応 |
Warn | 警告 | 廃要素となったAPIの使用、APIの不適切な使用、エラーに近い事象など。実行時に生じた異常とは言い切れないが正常とも異なる何らかの予期しない問題 | コンソール ファイル | 定期的な棚卸し |
Info | 情報 | 実行時の何らかの注目すべき事象(開始や終了などの処理内容) | コンソール ファイル | 対応不要 |
Debug | デバッグ情報 | システムの動作状況に関する詳細な情報(HTTPのRequest/ResponseやSQLのデバッグログなど) | (コンソール) ファイル | 出力しない |