ビジネスアプリ向けエラー分類:UIと監視の一貫性
ビジネスアプリ向けのエラー分類は、バリデーション、認証、レート制限、依存障害などを分類し、アラートとUIの挙動を一貫させます。

実際のビジネスアプリでエラー分類が解決すること
エラー分類は、問題を名前づけしてグループ化する共通のやり方です。画面やAPIごとに独自のメッセージを乱立させる代わりに、バリデーションや認証のような少数のカテゴリと、それをユーザーや監視でどう扱うかのルールを定義します。
共通の構造がないと、同じ問題が別々の形で現れます。必須項目の欠落が、モバイルでは「Bad Request」と表示され、Webでは「何かがおかしい」と出て、ログにはスタックトレースが並ぶ、というように。ユーザーは次に何をすべきか分からず、オンコールはそれがユーザーの操作ミスなのか攻撃なのか障害なのかを推測する時間を無駄にします。
目的は一貫性です。ある種類のエラーは同じUI挙動と同じアラート挙動につながるべきです。バリデーションは該当フィールドを指し示すべきで、権限の問題は処理を止めてどの権限が足りないかを説明すべきです。依存障害は安全なリトライや代替を提示しつつ、監視は正しいチームに通知します。
現実的な例:営業担当が顧客レコードを作ろうとしたら決済サービスが落ちている場合、アプリが汎用の500を返すと担当はリトライしてしまい、後で重複登録が発生するかもしれません。依存障害という明確なカテゴリがあれば、UIは「サービスが一時的に利用できません」と表示して重複送信を防ぎ、監視は適切なチームに通知できます。
この整合は、1つのバックエンドが複数クライアントを支えるときに特に重要です。API、Web、モバイル、社内ツールが同じカテゴリとコードを使えば、障害がランダムに見えることがなくなります。
単純なモデル:category、code、message、details
分類は、しばしば混在しがちな4つの要素を分けることで管理が楽になります:カテゴリ(どんな種類の問題か)、コード(安定した識別子)、メッセージ(人向けの文言)、詳細(構造化されたコンテキスト)。HTTPステータスは依然重要ですが、それだけが全てではありません。
Category は「UIと監視はどう振る舞うべきか?」に答えます。ある場所では403が「auth」を示すかもしれませんし、別の場所では後からルールを増やして「policy」を意味するかもしれません。カテゴリは振る舞いに関するもので、トランスポートではありません。
Code は「正確には何が起きたか?」に答えます。コードは安定していて退屈であるべきです。ボタン名を変えたりサービスをリファクタしてもコードは変わらないように。ダッシュボード、アラート、サポートスクリプトがこれに依存します。
Message は「人に何と伝えるか?」に答えます。誰向けのメッセージかを決めましょう。ユーザー向けは短く親切に。サポート向けは次の手順を含めてもよい。ログはより技術的でも構いません。
Details は「直すために何が必要か?」に答えます。詳細は構造化してUIが反応できるようにします。フォームエラーならフィールド名、依存問題なら上流サービス名と retry-after 値などです。
多くのチームが使う簡潔な形は次の通りです。
{
"category": "validation",
"code": "CUSTOMER_EMAIL_INVALID",
"message": "Enter a valid email address.",
"details": { "field": "email", "rule": "email" }
}
機能が変わっても、カテゴリは小さく安定させ、新しい状況は新しいコードを追加して表現してください。そうすることでUIの挙動、監視の傾向、サポートの手順が製品の進化に耐えられます。
コアカテゴリ:validation、auth、rate limits、dependencies
ほとんどのビジネスアプリは、どこにでも現れる4つのカテゴリから始められます。バックエンド、Web、モバイルで同じ名前と扱い方をすれば、UIは一貫して反応し、監視も読みやすくなります。
Validation(想定されるエラー)
バリデーションエラーはユーザー入力や業務ルールが満たされないときに発生します。これらは普通で、直しやすいはずです:必須項目の欠落、フォーマット不正、あるいは「割引は20%を超えられない」や「注文合計は $0 より大きくなければならない」といったルール。UIは汎用のアラートではなく、正確なフィールドやルールを強調するべきです。
認証と認可(想定されるエラー)
Auth エラーは通常、未認証(ログインしていない、セッション切れ、トークンなし)と権限不足(ログイン済みだが操作権がない)の2つに分かれます。これらは別々に扱いましょう。前者には「再度サインインしてください」が適切です。後者は機密情報を漏らさない範囲で明確にします:「請求書を承認する権限がありません。」
レート制限(想定されるが時間依存)
レート制限は「要求が多すぎます、後でやり直してください」を意味します。インポート、大量アクセスのダッシュボード、繰り返しのリトライなどでよく現れます。Retry-After のヒントを含め(たとえ「30秒待ってください」程度でも)、UIにはバックオフさせる仕組みを入れましょう。
依存障害(予期しないことが多い)
依存障害は上流サービスやタイムアウト、外部サービスの停止から来ます:決済プロバイダ、メール/SMS、データベース、社内サービスなど。ユーザーには直せないので、UIは安全な代替(下書き保存、後で試す、サポートに連絡)を提示するべきです。
重要なのは振る舞いの差です:想定内のエラーは通常のフローの一部で精密なフィードバックを与えるべきで、予期せぬエラーは不安定さのシグナルでありアラート、相関ID、注意深いログ出力を引き起こすべきです。
ステップバイステップ:ワークショップで分類を作る
分類は覚えられるほど小さく、かつ2つのチームが同じ問題を同じようにラベルできるほど厳密であるべきです。
1) 時間を区切って小さく始める
最初は60〜90分のワークショップにしましょう。よく見るエラー(入力不正、ログイン問題、多すぎるリクエスト、サードパーティの障害、予期せぬバグ)をリストアップし、それを6〜12のカテゴリに畳みます。誰でもドキュメントを見ずに声に出せる数にします。
2) 安定したコード命名を決める
ログやチケットで読みやすい命名パターンを選びます。短く、バージョン番号は避け、一度出したらコードは恒久的に扱いましょう。一般的なパターンはカテゴリ接頭辞+わかりやすいスラグ(例:AUTH_INVALID_TOKENやDEP_PAYMENT_TIMEOUT)です。
退出前に、すべてのエラーが含むべき項目を決めてください:category、code、安全なメッセージ、構造化されたdetails、traceまたはrequest ID です。
3) カテゴリとコードのルールを一つ作る
カテゴリがゴミ箱化するのを防ぐためにシンプルなルールが役立ちます:カテゴリは「UIと監視はどう反応するか?」に答え、コードは「正確に何が起きたか?」に答えます。異なるUI挙動が必要な二つの失敗は、同じカテゴリを共有すべきではありません。
4) カテゴリごとのデフォルトUI挙動を決める
デフォルトでユーザーに何を見せるかを決めます。バリデーションはフィールドを強調。認証はサインインかアクセスメッセージ。レート制限は「X秒後に再試行」を表示。依存障害は落ち着いたリトライ画面を示す。これらのデフォルトがあれば、新機能は個別の扱いを作らずに従えます。
5) 実際のシナリオでテストする
サインアップ、チェックアウト、検索、管理編集、ファイルアップロードのような代表的なフローを5つ動かし、それぞれの障害にラベルを付けます。議論になったら、多くの場合必要なのは20個のコードではなく、1つの明確なルールです。
バリデーションエラー:ユーザーが行動できる形にする
バリデーションは通常、即時に表示してよい唯一の失敗タイプです。予測可能で、ユーザーに何を直すかを教え、リトライループを引き起こさないようにします。
フィールドレベルとフォームレベルのエラーは別問題です。フィールドレベルは一つの入力(メール、電話、金額)に対応します。フォームレベルは入力の組み合わせ(開始日は終了日より前でなければならない)や前提条件の欠如(配送方法が選ばれていない)を表します。API応答はUIが正しく反応できるようにこの違いを明確にするべきです。
よくある業務ルール違反の例は「与信限度超過」です。ユーザーが有効な数値を入れていても口座状態で許されない場合があります。これはフォームレベルのバリデーションエラーとして扱い、理由と安全なヒント(「利用可能限度は $500 です。金額を下げるか増額を申請してください。」)を返しましょう。データベースの内部名やスコアリングモデル名、ルールエンジンの詳細はUIに出さないでください。
アクション可能な応答には通常、安定したコード(英語の文章だけでないもの)、ユーザー向けの分かりやすいメッセージ、フィールドポインタ(フィールドレベルなら)、そして小さな安全なヒント(フォーマット例、許容範囲)を含めます。エンジニア向けのルール名が必要ならログに入れ、UIには出さないでください。
バリデーション失敗のログはシステムエラーとは別に記録しましょう。パターンをデバッグするのに十分なコンテキスト(ユーザーID、リクエストID、ルール名またはコード、失敗したフィールド)を残しつつ、機密データは保存しないでください。値は必要最小限(例えば「存在/欠如」や長さ)だけをログにし、敏感な情報はマスクします。
UIでは「直すこと」に注力し、リトライを促さないでください。フィールドを強調し、ユーザーが入力した内容を保持し、最初のエラーまでスクロールし、自動リトライは無効にします。バリデーションエラーは一時的なものではないため「もう一度試す」は時間の無駄です。
認証と権限エラー:セキュリティと明瞭さを両立する
認証(Authentication)と認可(Authorization)の失敗はユーザーから見ると似ていますが、セキュリティ、UIフロー、監視では意味が異なります。この二つを分けることで Web、モバイル、API クライアント全体で一貫性が保たれます。
未認証はアプリがユーザーを証明できない場合です。典型的な原因は資格情報の欠如、無効なトークン、期限切れのセッションです。Forbidden(権限なし)はユーザーは判明しているが操作権がない場合です。
セッション切れが最も一般的なエッジケースです。リフレッシュトークンをサポートしているなら、まずサイレントリフレッシュを一度試し、成功したら元のリクエストを再実行します。リフレッシュが失敗したら未認証エラーを返してサインインに誘導します。ループを避けるため、リフレッシュは1回だけ試行し、失敗したら停止して明確な次の手順を表示してください。
UIの挙動は予測可能に保ちます:
- 未認証:サインインを促し、ユーザーが行っていた作業を保持する
- Forbidden:ページに留まり、アクセスメッセージを表示し「アクセスを要求する」などの安全な行動を示す
- アカウント無効化/取り消し:サインアウトして、サポートに連絡できる短いメッセージを表示する
監査のために「誰が何を試み、なぜブロックされたか」が答えられるだけの情報をログに残しますが、シークレットは含めないでください。記録に有用なのはユーザーID(分かっている場合)、テナントやワークスペース、アクション名、リソース識別子、タイムスタンプ、リクエストID、ポリシーチェックの結果(許可/拒否)などです。
ユーザー向けメッセージでは、役割名や権限ルール、内部ポリシー構造を明かさないでください。「請求書を承認するアクセス権がありません」は「Only FinanceAdmin can approve invoices」より安全です。
レート制限エラー:負荷時の予測可能な挙動
レート制限はバグではなく安全装置です。これを一つのカテゴリとして扱えば、トラフィックが急増したときにUI、ログ、アラートが一貫して反応します。
レート制限は次のような形で現れます:ユーザー単位(1人の連打)、IP単位(オフィスのネットワーク越しに多数)、APIキー単位(単一の統合ジョブが暴走)。原因によって対処が異なるため、区別が重要です。
良いレート制限応答に含めるもの
クライアントは自分が制限されたことと、いつ再試行すべきかを知る必要があります。HTTP 429 を返し、明確な待ち時間(例:Retry-After: 30)を含めましょう。さらに安定したエラーコード(例:RATE_LIMITED)を入れてダッシュボードがイベントをまとめられるようにします。
メッセージは落ち着いた具体的なものにしてください。「リクエストが多すぎます」だけでは不十分です。「30秒待ってから再度お試しください」は期待値を設定し、連続クリックを減らします。
UI側では速いリトライを防ぎます。シンプルなパターンは、待機時間中は操作を無効にし、短いカウントダウンを表示したのちタイマー終了後に1回だけ安全に再試行を提供することです。データが失われたと誤認させる表現は避けてください。
監視ではチームが過剰反応しがちです。すべての429で人を呼ぶべきではありません。レートの増加や、特定のエンドポイント/テナント/APIキーでの急増をアラートするのが実用的です。
バックエンドの振る舞いも予測可能にしましょう。自動リトライは指数的バックオフを使い、リトライは冪等にするか冪等化を考慮します。例えば「請求書作成」が二重に登録されないように設計してください。
依存障害:混乱なしに障害を扱う
依存障害はユーザー側で直せないものです。ユーザーは正しい操作をしているのに決済ゲートウェイがタイムアウトしたり、DB接続が切れたり、上流が5xxを返したりする場合です。これらは別カテゴリとして扱い、UIと監視の反応を予測可能にします。
まず障害の一般的な型を名付けます:タイムアウト、接続エラー(DNS、TLS、接続拒否)、上流の5xx(Bad Gateway、Service Unavailable)など。根本原因がわからなくても、何が起きたかを記録して一貫した応答を返せます。
リトライすべきか即失敗させるべきか
リトライは短期的な復旧に有効ですが、過度なリトライは障害を悪化させます。チーム間で同じ判断をするためのシンプルなルールを用意しましょう。
- 一時的である可能性が高いときはリトライ:タイムアウト、接続リセット、502/503
- ユーザー側や恒久的な問題は即失敗:依存先からの4xx、資格情報不正、リソースなし
- リトライ回数に上限(例:2〜3回)を設け、小さなバックオフを入れる
- 非冪等操作は idempotency キーがない限りリトライしない
UIの挙動と安全な代替
依存障害時はユーザーを責めずに次の手順を示します:「一時的な問題です。後でもう一度お試しください。」安全な代替があれば提供します。例:Stripeが落ちているなら注文を「支払い保留」として保存し、メールで確認を送るなど、カートを失わせない工夫をします。
また二重送信からユーザーを守ります。応答が遅い間に「支払う」を二度押しされた場合に備え、システム側で検出できる仕組みを作ってください。作成&課金のフローには idempotency キーを使うか、「すでに支払われている」などの状態チェックを行って重複を防ぎます。
監視向けのログには「どの依存が、どれくらいの深刻さで失敗しているか」を素早く答えられる項目を残しましょう:依存名、エンドポイントまたは操作、所要時間、最終結果(timeout、connect、upstream 5xx)。これによりアラートとダッシュボードが有用でノイズが少なくなります。
チャネル間でUIと監視を一貫させる
分類は全チャネルが同じ言語を話したときにのみ機能します:API、Web UI、モバイル、ログ。さもないと同じ問題が5つの別々のメッセージになり、誰もそれがユーザーのミスか本当の障害か判断できません。
HTTPステータスコードは二次的な層として扱ってください。プロキシや基本クライアント挙動には役立ちますが、意味はカテゴリとコードに持たせるべきです。依存のタイムアウトは503でも、カテゴリがUIに「再試行を提示」し監視にはオンコールを鳴らすよう指示します。
ソースが何であれ(データベース、認証モジュール、サードパーティAPI)、すべてのAPIが一貫したエラー形を返すようにしましょう。シンプルな形はUI処理とダッシュボードを一貫させます:
{
"category": "dependency",
"code": "PAYMENTS_TIMEOUT",
"message": "Payment service is not responding.",
"details": {"provider": "stripe"},
"correlation_id": "9f2c2c3a-6a2b-4a0a-9e9d-0b0c0c8b2b10"
}
相関IDは「ユーザーがエラーを見た」ことと「我々がそれを追跡できる」ことをつなぐ橋です。相関IDはUIに表示(コピー用ボタンが役立ちます)し、バックエンドでは必ずログに残して、1つのリクエストをサービス横断で追えるようにしてください。
UIに表示してよい情報とログ内に留める情報を合意しておきましょう。実用的な分け方は:UIにはカテゴリ、明確なメッセージ、次の手順を表示し、ログには技術的なエラー詳細とリクエストコンテキストを残す。両方に相関IDと安定したエラーコードを含めます。
一貫したエラーシステムのためのクイックチェックリスト
一貫性は最良の意味で地味です:すべてのチャネルが同じ振る舞いをし、監視が真実を伝えるようになります。
まずバックエンド(バッチジョブやWebhookを含む)をチェックしてください。フィールドが任意だと人は省略してしまい、一貫性が崩れます。
- すべてのエラーにカテゴリ、安定したコード、ユーザー安全なメッセージ、トレースIDが含まれている
- バリデーションは想定内なのでページングアラートを発生させない
- 認証/権限はセキュリティパターンとして追跡するが障害扱いにはしない
- レート制限応答には再試行ヒント(秒数など)を含めてアラートを乱発しない
- 依存障害には依存名とタイムアウトやステータスの詳細を含める
次にUIルールをチェックします。各カテゴリはユーザーに対して予測可能な画面挙動にマップされるべきです:バリデーションはフィールドを強調、認証はサインインまたはアクセス表示、レート制限は落ち着いた待機表示、依存障害は可能なリトライと代替を提示します。
簡単なテストは、ステージングで各カテゴリのエラーを一度ずつ発生させ、Webアプリ、モバイル、管理画面で同じ結果が得られるか検証することです。
よくある間違いと実践的な次の一手
エラーシステムを後回しにするのが最速で壊す方法です。チームごとに違う言葉やコード、UI挙動を使うと同じ問題がバラバラに扱われます。分類への投資は、一貫性が保たれて初めて報われます。
よくある失敗パターン:
- 内部例外テキストをそのままユーザーに出してしまう。混乱を招き、機密情報を露出する危険がある。
- すべての4xxをまとめて「バリデーション」とラベルする。権限不足は必ずしも入力ミスではない。
- 機能ごとに新しいコードを無計画に作ると、結局同じ5つの意味に対して200個のコードができる。
- 間違った失敗をリトライする。権限エラーや不正なメールアドレスをリトライしてもノイズが増えるだけ。
簡単な例:営業が「顧客作成」フォームを送信して403を受け取ったとします。UIがすべての4xxをバリデーション扱いすると、ランダムなフィールドをハイライトして「入力を修正してください」と出してしまい、本当の問題はアクセス権です。監視は「バリデーション問題」の急増として表示されますが、実際はロール関連の問題です。
短いワークショップでできる実践的な次の一手:1ページの分類ドキュメントを書く(カテゴリ、使いどころ、5〜10の代表コード)、メッセージルールを定義(UIに出すもの vs ログに残すもの)、新しいコードの軽いレビュープロセスを入れる、カテゴリごとのリトライルールを決める、そしてエンドツーエンドで実装する(バックエンド応答、UIマッピング、監視ダッシュボード)。
AppMaster(appmaster.io)で構築しているなら、これらのルールを一箇所に集中させることで、バックエンド、Web、ネイティブモバイル全体で同じカテゴリとコードの振る舞いを保ちやすくなります。
よくある質問
バックエンドが複数のクライアント(Web、モバイル、社内ツール)にサービスを提供し始めたとき、あるいはサポートやオンコールが「これはユーザーの入力ミスかシステム障害か?」と繰り返し問うようになったら始めどきです。サインアップ、チェックアウト、インポート、管理編集のような繰り返しのあるフローがあると、分類はすぐに効果を発揮します。
覚えやすくドキュメントを見ずに言えるように、まずは6〜12のカテゴリから始めるのが良いです。カテゴリは広く安定させ(validation、auth、rate_limit、dependency、conflict、internalなど)、具体的な状況はコードで表現しましょう。
カテゴリは挙動(UIや監視がどう反応するか)を決め、コードは発生理由を特定します。カテゴリは「どう対応すべきか」を示し、コードはダッシュボードやアラート、サポートスクリプトで長期間使える識別子です。
メッセージは識別子ではなくコンテンツです。UI向けには短く安全な文言を返し、分類やグルーピング/自動化には安定したコードを使いましょう。技術的な文言が必要ならログに残し、同じ correlation ID に結び付けます。
カテゴリ、安定したコード、ユーザー向けの安全なメッセージ、構造化された詳細、そして相関/リクエストIDを含めてください。詳細はクライアントが対処できる形(どのフィールドが失敗したか、どれくらい待つか)に整え、内部の例外テキストをそのまま返すのは避けます。
可能な限りフィールド単位の情報を返し、UIがどこを修正すればよいかを示せるようにします。フォーム全体に関わるルール違反(期間の順序や与信限度など)はフォームレベルのエラーとして返し、UIが誤って単一フィールドをハイライトしないようにします。
未認証(not logged in/トークン無効等)はサインインを促して実行中の作業を保持し、許可なし(forbidden)はページに留めてアクセス不足の旨を表示します。セキュリティ上、役割名やポリシー構造は開示しないでください。
明示的な待ち時間(例えば retry-after 値)を返し、安定したコードを使ってクライアントが一貫したバックオフを実装できるようにします。UI側では連打を防ぎ、次の手順を明示してください。自動的な速いリトライは状況を悪化させます。
一時的な障害(タイムアウト、接続リセット、上流の502/503)に対してのみリトライし、試行回数は上限を設けてバックオフを入れます。非冪等の操作は idempotency キーや状態チェックを必要としない限り再試行しないでください。さもないと重複が発生します。
ユーザーに相関IDを見せて(サポートが求めやすいように)、サーバー側では必ず同じ相関IDでログを残してください。これにより障害をサービス間で辿れます。AppMaster のプロジェクトでは、この形を一元化するとバックエンド、Web、ネイティブでの挙動が一致しやすくなります。


