php elephant sticker
Photo by RealToughCandy.com on Pexels.com
目次

【現場完全ガイド】Laravelのエラーハンドリングと障害対応――例外設計、エラーページ、APIエラー、ログ/監視、再試行、メンテナンス、アクセシブルな復旧導線

この記事で学べること(要点)

  • Laravelの例外処理(Handlerreportable/renderable)を「運用に強い形」に整える方法
  • 404/419/429/500/503 など代表的エラーの意味と、ユーザーに優しいエラーページ設計
  • APIのエラーフォーマット統一(problem+json)とトレースIDでの調査短縮
  • ログの構造化、PIIマスキング、監視/アラート、障害時の一次対応(Runbook)
  • 再試行・フォールバック・キュー/外部API障害の扱い
  • エラー時でも迷子にしない、アクセシブルな案内(フォーカス/読み上げ/色非依存/次アクション提示)

想定読者(だれが得をする?)

  • Laravel 初〜中級エンジニア:例外処理を「とりあえず」から卒業し、障害に強い実装を作りたい方
  • テックリード/運用担当:監視とログを整備し、調査時間と復旧時間(MTTR)を短縮したい方
  • デザイナー/ライター/QA:エラーページや再試行導線を、誰にでも分かる表現へ統一したい方
  • API連携担当:クライアントが扱いやすいエラー形式と、問い合わせが減る情報設計を整えたい方

アクセシビリティレベル:★★★★★

エラーの見出し・要約・次の手段、role="alert"/role="status"、フォーカス移動、色に依存しない表現、メンテ中の案内、再試行UIまで具体例を含めています。


1. はじめに:エラーは「起きる前提」で設計すると強くなります

障害対応で一番つらいのは、原因が分からないことと、ユーザーが迷子になることです。Laravelは例外処理の仕組みが整っている一方で、放っておくと「ログに情報が足りない」「同じ500でも理由が見えない」「APIと画面でエラー形式が違う」「エラーページが不親切」という状態になりがちです。

本記事では、エラーを“単なる失敗”ではなく「状況を説明して復旧へ導く機能」と捉え直し、実装・運用・アクセシビリティを一緒に整える型を紹介します。


2. 設計方針:エラーを3つに分類して扱う

現場で整理しやすい分類は、次の3つです。

  • ユーザーが直せる(入力ミス、権限不足、期限切れ)
    • 例:422、401/403、419
    • 望ましい挙動:何を直せばよいかを短文で、次の操作を提示
  • 一時的に待てば直る(混雑、外部API障害、メンテ)
    • 例:429、503
    • 望ましい挙動:待ち時間・再試行方法・代替手段を提示
  • ユーザーには直せない(バグ、想定外、サーバ障害)
    • 例:500
    • 望ましい挙動:謝意+影響範囲+問い合わせに必要なID、復旧導線

この分類で「画面の文言」「HTTPステータス」「ログ」「アラート」を揃えると、チームの会話が速くなります。


3. Laravelの例外処理の基本:Handlerの責務を明確にする

Laravelでは主に app/Exceptions/Handler.php が入口です。ここでやりたいことは大きく2つです。

  • report:運用向け(ログ、監視、Sentry等へ通知)
  • render:ユーザー向け(画面/JSONの応答を整える)

3.1 「報告しない例外」を決める

入力エラーや認可エラーを毎回エラー通知するとノイズになります。運用で価値があるのは、主に「想定外」と「急増」です。
Laravelの dontReport(または reportable 条件)で、通知対象を絞ります。

// app/Exceptions/Handler.php(一部)
protected $dontReport = [
    \Illuminate\Validation\ValidationException::class,
    \Illuminate\Auth\Access\AuthorizationException::class,
    \Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class,
];

4. トレースIDで「問い合わせ→調査」を最短にする

ユーザーから「エラーになりました」と言われたとき、再現できないのが一番困ります。そこで、すべてのリクエストに trace_id(または request_id)を付け、画面にもAPIにも返します。

4.1 ミドルウェアでトレースID付与

// app/Http/Middleware/TraceId.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Str;

class TraceId
{
    public function handle($request, Closure $next)
    {
        $traceId = $request->header('X-Trace-Id') ?: 'req-'.Str::uuid()->toString();
        $request->attributes->set('trace_id', $traceId);

        $response = $next($request);
        $response->headers->set('X-Trace-Id', $traceId);

        return $response;
    }
}
  • これを webapi に適用すると、調査が一気に楽になります。
  • 画面側にも「お問い合わせの際はこの番号をお伝えください」と出せます。

5. 画面向け:エラーページを“復旧導線”として設計する

5.1 404(Not Found)

ユーザーの操作ミスか、リンク切れです。やるべきことは次の3点です。

  • 何が起きたか(ページが見つからない)
  • 次に何をすればよいか(トップへ、検索へ、戻る)
  • 可能なら原因(URLが変更された可能性)
{{-- resources/views/errors/404.blade.php --}}
@extends('layouts.app')
@section('title','ページが見つかりません')

@section('content')
  <main aria-labelledby="error-title">
    <h1 id="error-title" tabindex="-1">ページが見つかりません</h1>
    <p>URLが変更されたか、入力が間違っている可能性があります。</p>

    <ul>
      <li><a href="{{ route('home') }}">トップページへ戻る</a></li>
      <li><a href="{{ route('products.index') }}">商品一覧から探す</a></li>
    </ul>
  </main>
@endsection

アクセシビリティのポイント

  • 見出しに tabindex="-1" を付け、ページ遷移後にフォーカスを当てやすくします。
  • 色だけで状態を示さず、短文で説明します。

5.2 419(Page Expired:CSRF/セッション期限)

フォーム送信のやり直しが必要なケースです。ここで大切なのは「責めない文言」と「復旧手順」です。

  • 「長時間操作がなかったため、もう一度お試しください」
  • 入力が失われる可能性を明示し、可能なら下書き/復元を案内

5.3 429(Too Many Requests:混雑/連打)

「待てば直る」を伝え、Retry-After の秒数が出せるなら表示します。

5.4 503(メンテナンス/一時停止)

メンテ中は「いつ頃戻るか」「影響範囲」「緊急連絡手段」を明確にします。
また、メンテページは軽量で、画像や複雑なJSに依存しない方が安定です。


6. API向け:エラー形式を統一する(problem+json)

APIは「ステータスコード+本文」で判断されます。本文がバラバラだと、クライアントは例外処理が肥大化します。
おすすめは RFC 7807 の application/problem+json 形式です。

6.1 例:バリデーションエラー(422)

{
  "type": "https://example.com/problems/validation",
  "title": "Validation Failed",
  "status": 422,
  "detail": "入力内容を確認してください。",
  "errors": {
    "email": ["メールアドレスは必須です。"]
  },
  "trace_id": "req-..."
}

6.2 HandlerでJSONレスポンスを統一(概略)

// app/Exceptions/Handler.php(概略例)
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;

public function render($request, Throwable $e)
{
    if ($request->expectsJson()) {
        $traceId = $request->attributes->get('trace_id');

        $status = $e instanceof HttpExceptionInterface ? $e->getStatusCode() : 500;

        $payload = [
            'type' => $this->problemType($e, $status),
            'title' => $this->problemTitle($status),
            'status' => $status,
            'detail' => $this->problemDetail($e, $status),
            'trace_id' => $traceId,
        ];

        if ($e instanceof ValidationException) {
            $payload['type'] = 'https://example.com/problems/validation';
            $payload['status'] = 422;
            $payload['title'] = 'Validation Failed';
            $payload['detail'] = '入力内容を確認してください。';
            $payload['errors'] = $e->errors();
        }

        return response()->json($payload, $payload['status'])
            ->header('Content-Type', 'application/problem+json');
    }

    return parent::render($request, $e);
}

ポイント

  • type はエラー説明ページに紐づく固定URLにすると、サポートと開発が揃います。
  • trace_id を返すと、問い合わせの往復が減ります。

7. ログ設計:読む人は未来の自分です

7.1 構造化ログにする

ログは「文章」より「キー・値」が検索しやすいです。

Log::error('api.failed', [
  'trace_id' => request()->attributes->get('trace_id'),
  'user_id' => optional(auth()->user())->id,
  'path' => request()->path(),
  'method' => request()->method(),
  'status' => 500,
  'exception' => get_class($e),
]);

7.2 PIIをマスクする

メールや住所などをそのままログに出すと危険です。
方針として「ログに残すのはIDまで」を基本にし、必要ならマスクします。


8. 外部API障害:タイムアウト、再試行、フォールバック

外部APIは落ちます。落ちる前提で作ると、障害が“致命傷”になりにくいです。

8.1 HTTPクライアントの基本

  • タイムアウトを明示
  • 再試行は指数バックオフ
  • 失敗時は「機能を止める」のではなく「縮退」できる場所を決める
$res = Http::timeout(10)
  ->retry(3, 200, function ($exception, $request) {
      return true; // 条件で絞るのが理想
  })
  ->get('https://api.example.com/data');

if ($res->failed()) {
    // 例:キャッシュした前回値を返す(フォールバック)
    $cached = Cache::get('external:data');
    return $cached ? $cached : null;
}
Cache::put('external:data', $res->json(), 300);
return $res->json();

9. キュー/ジョブの失敗:リトライとデッドレターの考え方

  • 一時障害(ネットワーク)→再試行
  • 恒久障害(データ不正)→失敗として隔離し、人が見る

Laravelのジョブは tries/backoff/timeout を明示すると運用が安定します。

class SendInvoiceMail implements ShouldQueue
{
    public $tries = 5;
    public $timeout = 120;

    public function backoff(): array
    {
        return [10, 30, 60, 120, 300];
    }

    public function handle()
    {
        // 送信処理
    }
}

アクセシブルな観点(ユーザー向け)

  • 「送信中です…」→「完了しました」→「失敗しました(再試行/問い合わせ)」を、画面や通知で短く伝える
  • 進捗や結果は role="status" で読み上げ可能にしておくと、状況把握が楽になります

10. エラー時のUI:迷子にしないための“最低限セット”

画面での最低限セットは、次の4つです。

  • 見出し:何が起きたか
  • 要約:1〜2文
  • 次の手段:リンク/ボタン
  • 問い合わせ情報:trace_id(必要なら)

10.1 重要なエラーは role="alert"

フォームエラーのサマリなどは role="alert" を使うと伝わりやすいです。ただし乱用は避け、緊急性の高い箇所に限定します。

@if(session('error'))
  <div role="alert" class="border p-3">
    {{ session('error') }}
  </div>
@endif

10.2 ローディング失敗の案内は role="status"

「再読み込み」「もう一度試す」「軽量版を使う」を用意すると、行き止まりが減ります。


11. 監視とアラート:何を見れば“気づける”か

最低限おすすめの指標は次のとおりです。

  • 5xx率(急増検知)
  • 429率(混雑や誤実装の兆候)
  • 外部API失敗率とタイムアウト数
  • キューの遅延(待ち行列が詰まっていないか)
  • DBのスロークエリ

アラートは「通知が多すぎる」と無視されます。最初は

  • 5xx急増
  • キュー遅延の悪化
  • 主要外部APIの失敗率上昇
    この3つから始めると運用が回りやすいです。

12. Runbook(一次対応手順)を用意する

障害対応は、手順があるだけで落ち着きます。短くていいので、次を決めておくと強いです。

  • 影響範囲の確認(どの機能、どのユーザー)
  • ログ検索の入口(trace_id、エンドポイント、例外クラス)
  • 直近デプロイ差分の確認
  • ロールバック判断基準
  • ユーザーへの案内テンプレ(メンテページ/ステータスページ)

13. テスト:エラーは“仕様”として固定する

13.1 Featureテスト(例:422)

public function test_api_validation_problem_json()
{
    $res = $this->postJson('/api/v1/users', ['email' => '']);
    $res->assertStatus(422)
        ->assertHeader('Content-Type', 'application/problem+json')
        ->assertJsonStructure(['type','title','status','detail','errors','trace_id']);
}

13.2 429や503もテスト観点に入れる

  • レート制限時に Retry-After が付く
  • メンテモード時に適切なページが出る
  • 画面のエラーに「次の手段」がある

14. よくある落とし穴と回避策

  • 例外を握りつぶす(try/catchで空にする)
    • 回避:握りつぶすなら必ず「代替挙動」と「ログ」をセットに
  • 500で同じ文言しか出ない
    • 回避:分類(直せる/待てる/直せない)で文言と導線を変える
  • ログに情報が無い、または多すぎる
    • 回避:trace_id+最低限のキーを固定、PIIは出さない
  • APIと画面でエラー形式が違いすぎる
    • 回避:APIはproblem+jsonで統一、画面は復旧導線重視
  • 監視がアラート地獄
    • 回避:急増系と重大系から始め、段階導入

15. チェックリスト(配布用)

例外/応答

  • [ ] trace_id を全レスポンスに付与
  • [ ] APIは application/problem+json を統一形式に
  • [ ] 404/419/429/500/503 のエラーページを用意し、次アクションがある

ログ/運用

  • [ ] 構造化ログ(trace_id, user_id, path, status, exception)
  • [ ] PIIマスキング方針(ID中心)
  • [ ] 主要指標(5xx、429、外部API失敗、キュー遅延)を監視
  • [ ] Runbook(一次対応)を短くても用意

信頼性

  • [ ] 外部APIは timeout/retry/fallback を定義
  • [ ] ジョブは tries/backoff/timeout を明示
  • [ ] メンテモード時の案内(503)を準備

アクセシビリティ

  • [ ] 見出し+要約+次の手段+問い合わせID
  • [ ] 重要エラーは role="alert"、進捗は role="status"
  • [ ] 色だけに依存しない表現、キーボードで導線を辿れる

16. まとめ:エラーは“復旧への案内”として育てましょう

  • エラーは起きる前提で分類し、画面文言・HTTP・ログ・監視を揃えると強くなります。
  • trace_id を全体に通すだけで、調査の速度が大きく上がります。
  • APIはproblem+jsonで統一し、クライアントの例外処理を単純化します。
  • エラーページは行き止まりにせず、必ず次の手段を提示します。
  • 外部API・キュー・混雑は“待てば直る”設計にし、ユーザーに状況を分かりやすく伝えます。
  • アクセシビリティは、障害時ほど効きます。落ち着いて操作できる案内を標準にしましょう。

参考リンク

投稿者 greeden

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)