はじめに
Webサービスや業務システムを開発する中で、必ず直面するのが「エラーハンドリングの設計」です。
単に「例外が起きたら500を返す」で終わらせると、ユーザー体験もデバッグ効率も大きく損なわれます。
この記事では、以下の観点からエラー設計を整理します。
- バリデーションチェックをフロントでやるか、バックでやるか
- 操作できない場合はエラーを返すべきか?
- HTTPステータスの使い分け
バリデーションチェックはフロント?バック?
フロントでチェックするメリット
ユーザー体験を考えると、入力直後にその場でエラーを伝えられる方がスムーズ。
余計なリクエストをサーバーに送らずに済むので効率的。
一方で、バックエンドでのチェックでは、たとえフロントのチェックをすり抜けても、必ず最後に検証が走る「信頼できる最終ゲート」として機能する、かつDBの参照や他サービスとの連携が必要な複雑な検証も扱うことができる。
そのため、**基本は「フロントでできる範囲は即時チェック、バックで必ず最終チェック」**という二段構えが良さそう。
両方で同じようなバリデーションを書くのは、一見冗長に見えるが、ユーザー体験と安全性を両立させるためには両方書くが良さそう!
操作できない場合はバックエンドからエラーを返す?返さない?
「エラーとして返すか、操作不可状態にしてリクエスト自体を送らせないか」という悩みが一瞬あった。
実際の悩んだものではないが、わかりやすい例で言うと、下記のような事例。
予約リクエストを送ったタイミングで「その枠は埋まりました」とバックエンドで返す。
→ そもそもフロント側カレンダー画面で他人がすでに予約した枠は選べないように非表示にする方が良くない?
バックエンドから返す
何らかの方法で、操作はできてしまうが、サーバー側で必ず止めたい状況。
例えば「一般ユーザーが管理者用の削除操作を実行しようとする」ケース。
UIでボタンを隠していても、直接リクエストを送られれば操作は届いてしまうので、サーバー側で権限チェックをして必ず弾く必要がある。
「在庫がゼロの商品を同時に注文する」ようなケースも同じく。
画面では購入ボタンを押せるように見えていても、複数人が同時にアクセスすれば競合が発生するため、サーバー側で最終的にブロックする仕組みが欠かせない。
UIで抑止するケース
一方で、そもそも操作してほしくないものはUIで制御した方がスムーズ。
「すでに承認済みの申請」に対しては、ボタンを非活性にして再承認できないようにする方が直感的。
ここで無理にリクエストを送らせて「承認済みです」とエラーを返すのは、ユーザー体験的にはただの無駄足になる。
結論
想定外の入力やリクエストはサーバーでエラーにする。想定内で禁止できるものはUIで制御する。
そのうえで、フロントでできる限り制御、バックでも必ず制御という二重構えが基本。
HTTPステータスの使い分け
ステータス | 意味 | 代表例 | プラスアルファ設計指針 |
---|---|---|---|
400 Bad Request | リクエストが不正 | クエリパラメータが壊れている、JSONがパースできない | 入力以前の不正と区別(422は業務ルール違反、400は形式不正) |
401 Unauthorized | 認証が必要 | アクセストークンなし、期限切れ | レスポンスには「再認証が必要」であることを明示 |
403 Forbidden | 認可されていない | 権限不足、管理者専用APIへのアクセス | 存在しないかアクセス不可かをポリシーに応じて選択(404返す場合も) |
404 Not Found | リソースが存在しない | 存在しないID、削除済みデータ | 監査ログに記録し、403との切り分けを明確にする |
409 Conflict | リソース状態と矛盾 | 在庫0購入、楽観ロック失敗 | ユーザー再操作で解決できることを伝えるメッセージを返す |
422 Unprocessable Entity | リクエストは正しいが処理できない | 必須項目、形式不正、重複登録 |
フィールドごとに詳細返却(例: errors: { email: ["すでに登録されています"] } ) |
429 Too Many Requests | レート制限超過 | API叩きすぎ | |
500 Internal Server Error | サーバー内部の想定外エラー | サーバー異常、API異常 | クライアントには「システムエラー」、ログには詳細(スタックトレース*)を残す |
504 Gateway Timeout | 外部サービス応答なし | 外部APIタイムアウト | |
※スタックトレース:「どの関数がどんな順番で呼ばれて、どこでエラーになったのか」を記録した履歴。 |
最終的には、「ユーザーにとって行動が明確になるレスポンス」「開発者にとって原因が追いやすいログ」 を両立させることが重要!
403と404の補足
補足
APIで「存在するけど権限がない」と返してしまうと、攻撃者に「そのIDは存在する」と教えてしまうことになる。
そのため、セキュリティポリシーによっては、本当は403のケースでも404を返すことがある。
(例)
不正ユーザーが「user_id=1〜100」を順番に叩くと、403と404の違いで「存在するユーザーIDの一覧」がわかってしまう
→ これを防ぐために、存在していても「Not Found」で返す
対策
クライアントには「404」とだけ返す。
内部の監査ログには「リソースは存在していたが403の状態」と残す。
まとめ
エラーハンドリングは、ユーザーにも開発者にも大きく影響するため自身が思っている以上に大切だということを学びました。
だからこそ、"最初"に方針を決めてチームで共有しておくのが一番大事だなと感じました。