メインコンテンツまでスキップ

Error ハンドリング戦略

JavaScript/TypeScriptの例外ハンドリング戦略を考える

例外設計についてまとめる
例外設計を行う時はまずは例外を業務エラーとシステムエラーに分類することから考えていく。
例外というのはすべて、何かしらのリカバリーを考える必要がある。

アプリケーションがクラッシュするとは

アプリケーションがクラッシュする場合、一般的にはエラーがキャッチされずに実行コンテキストから外れることが主な原因

具体的な条件としては以下のようなケースが考えられる。

  1. エラーが発生し、そのエラーがトップレベルのエラーハンドラーに到達しない場合 エラーが発生しても、エラーハンドリングが適切に行われていない場合に起こります。エラーがキャッチされず、処理の制御が外部に移されるため、アプリケーションはクラッシュします。

  2. 非同期処理のエラーが処理されずに実行コンテキストから外れる場合:非同期処理(例: Promiseのrejectやコールバック関数内のエラー)が発生し、エラーハンドリングが適切に行われない場合に起こります。非同期処理内でエラーが発生し、そのエラーがキャッチされずに処理の制御が外部に移されると、アプリケーションはクラッシュします。

  3. 例外がスローされ、キャッチされずに実行コンテキストから外れる場合:JavaScriptの throw 文を使用して明示的に例外をスローし、その例外がキャッチされずに処理の制御が外部に移されると、アプリケーションはクラッシュします。

Node.jsでは、未処理の例外がキャッチされなかった場合、デフォルトの動作としてアプリケーションが終了し、スタックトレースやエラーメッセージが出力されます。

したがって、アプリケーションがクラッシュしないようにするためには、エラーハンドリングを適切に行い、エラーがキャッチされるようにすることが重要です。例外やエラーをキャッチして適切な処理を行うことで、アプリケーションの安定性を確保することが可能。

例外の分類

参考URL

大きく分けると2つ

  • 正常系 目的の出力や副作用を得ることができる系
  • 業務エラー
  • 准正常系(想定内) 異常系であるが、仕様どおりに正常系への回復動作が実行される。 あるいは、仕様どおりにプロセスが終了する系(異常系だが想定内だと準正常系になる)
  • システムエラー(500番)
  • 異常系 目的の出力や副作用を得ることができない系

業務エラー

主にユーザの誤入力・誤操作が原因

  • 指定フォーマットの違い(メールアドレスなど)
  • 一意チェック(ユーザの重複など)
  • 必須項目の存在

業務エラー対処方法

業務エラーとは主にユーザーの誤入力・誤操作が原因のエラーであり、その対処方法としてはユーザーに誤入力・誤操作を取り消してもらい、正常な入力・正常な操作を行ってもらう事が対処方法になる。 その為には正常な入力・正常な操作を行ってもらえるような例外(エラー)を考えていく必要がある。 正常な入力・正常な操作を行ってもらうには、まず何が誤入力・誤操作だったのかをユーザーに指摘する事が考えられるでしょう。 たとえばメールのフォーマットが違う場合、『エラーが起きました。』のような抽象的なメッセージを表示するのではなく、『不正なメールのフォーマットです。』のような具象的なメッセージを画面を用いて表示させる事が適切です。

システムエラー

システムエラーはユーザー側でどうにもできないエラーというメッセージ(いわゆる「500エラー画面」)を表示する。

  • サーバが停止している
  • データベースに接続できない
  • プログラムのバグ

システムエラー対処方法

システムエラーとは主にユーザー側で対処できないエラーです。

たとえばプログラム内部のバグが原因でエラーが起きたとしましょう。 その時はシステム開発者がプログラムを修正する事が対処方法になります。 ユーザー側はバグを修正することは不可能な為、開発者にエラーが発生した詳しい状況を伝えてもらえるような画面を表示する必要がある。
また、バグが原因のエラーが発生した後にプログラムを実行してしまうと以降のプログラムの実行結果が正しいことが保証できない為、安全に終了してもらう必要があります。 開発者側は、システムエラーが起きたときに素早く対処できるように、ログにエラー内容を出すようにしましょう。
または、Errbitなどのエラー監視サービスを導入し、Slackなどと連携しておくとエラー時に迅速な対処が行えるでしょう。

エラーハンドリングの処理で考えられること

大きく分けて2つある

  • try catchで対応する方法 これの利点は集中管理ができること。throwをするとcatchが宣言されているところまでジャンプができる。 そのため集中管理が向いている。しかし場合によっては独自のエラーを使用したいなどがありコードが煩雑になる。

  • エラーとみなす値を返す Rustなどはこっちの方針。undefined などを使用し、エラーと認識させ次の処理をする。 こっちのがコードが綺麗になる。

try catchを使わない方法

参考URL

Result型(言語によっては実装されている)を実装する方法がある。

Go言語にtry-catchがないのはこういうこと?って気づきを感じました!

リトライ

あまりエラーの種別を細かく判定してあげることはJavaScriptでは今までやってこなかったのですが、ちょっとしたメタデータを乗っけてあげるとか(たとえばリトライ回数)、何か凝ったことをしたくなったらこういう方針でやればいいのでは

エラーと例外の区別が必要か

エラーと例外の違いとか、こっちはハンドリングするもの、こっちはOSにそのまま流すものとか色々な議論が出てくる。

アプリ運用のエラー監視

サーバーサイドのエラー監視については導入しているアプリケーションも多いが、フロントのエラー監視もする必要がある。

error 設計単語

参考URL

  • 正常系
  • 半正常系
  • 異常系

異常系はさらに2つに分けられる

業務エラー システムエラー


Tool 群について

Sentry

参考URL

フロントエラーの監視ツール。 無料枠が結構多い。

memo

設定されているか(contact pageで設定されているか確認する) or 送信されている(スコアのresponseが返ってきてるか) サーバ側でレスポンスがスコアの判定が返されている訳ではないため

status

リファレンス

ステータスコードの意味 100番台 情報レスポンス 200番台 成功レスポンス(HTTPメソッドによって少し変わる) 300番台 リダイレクト 400番台 クライアント再度に起因するエラー 500番台 サーバサイドに起因するエラー

重複エラーを伝える英語フレーズ

参考URL

error ハンドリング

参考URL

エラーとは

エラーとは

  • ファイルハンドラーオープンの失敗
  • メモリやリソース確保の失敗
  • 不正なメモリアクセス
  • 論理的計算の間違えなど

エラー伝達方法

1or2が一般的。

  1. 例外をthrow
  2. 関数の戻り値でエラー状態を返す

どこでハンドリングするかの考え方

  1. 論理エラー プログラムの論理的矛盾により発生(要はund

  2. 実行時エラー(ランタイム)

成功ステータスコードの返却(デファクトスタンダード)

利用するHTTPメソッドによって細かく分かれている

200: GETまたはPATCHリクエストに対し返す

201: Created」で「リクエストは成功し、新しいリソースが作られた」と言う意味を表す。一般的にPOSTでデータ追加した時に返す事が多い。

204:「No Content」で「コンテンツなし」と言う意味を表します。DELETEでデータ削除した時に返す事が多い様です。

api server error設計

dev環境の場合はrescueせずに例外をそのまま流し、Response bodyにstack traceなど例外情報を載せる これがいいっぽい。Sentryに流している場合はそれにも流さないといけないな

途中でエラーが発生しても続行したいケース

たとえばメールの一斉送信を行ったりする場合は、途中でエラーになるユーザーがいてもそこで処理を中止せず、残りのユーザーにメールを送信したい。 こういう場合はエラーをrescueして、処理を続行する。

# 全ユーザーに対して「本日の更新情報」を送信する
def self.send_daily_summary_to_all_users
User.all.each do |user|
begin
UserMail.daily_summary(user).deliver
rescue => e
# 何らかのエラーが発生した場合はログの書き込みと、
# エラー通知サービスへの通知を行う
logger.error e
logger.error e.backtrace.join("\n")
Bugsnag.notify e

# エラーが起きても次のユーザーの処理へ進む
end
end
end

例外処理のデザインパターン

参考URL
実装はこれを使った

第一前提として静的型付け言語であれば、そのメソッドが例外を吐く可能性があるのかがわからないといけない(Javaはわかりやすい)

  1. 言語として想定されているのは主に例外を投げるパターン

例外を投げるパターンでは throw new Error("message") というように失敗した理由を一緒に投げられる一方、その関数を使う側からみると返り値の型からその関数が失敗する可能性があることを推測することができません。

  1. undefinednull とのユニオンを使うパターン

undefined や null とのユニオンを使うパターンでは、関数を使う側からみて失敗する可能性が分かり、なおかつ ?. 演算子や ?? 演算子など言語自体からのサポートも手厚いです。一方失敗した理由を投げることはできません。

ここで、Rustでいう Result 型、HaskellやScalaでいう Either 型を自前で実装し使うことができれば、失敗の可能性を型で表現できるだけでなく、失敗の理由も一緒に入れてあげることができるという安直なアイディアが出てきます。

Either Monad(イーザーモナード)

Either Monadとは失敗する可能性のある計算について、その結果と失敗の理由両方を一度に表現できる型。

TypeScriptにはEither型がありません。 通常はUnion Typesを使ってResult | Errorのような表現をします。

一方、関数型言語では同じことを表すのにEither型を使うことが多い。
Eitherは文脈を持つコードを書けるので、上手く使えばスマートになります。

Errorのレスポンスを考える

配列で返すのもいいかも。

Twitterはエラーが配列で返るようになっています。これは複数のエラーが同時に発生した場合に合理的な方法といえます。たとえば、パラメータが2箇所間違っていた場合に、2箇所のパラメータ違いを別途エラーとして指定するほうが、開発者にとっては親切なことだといえるからです。

ステータスコードの扱い

ステータスコードはヘッダーに含むので不要になります。