【現場完全ガイド】Laravelのエラーハンドリングと障害対応――例外設計、エラーページ、APIエラー、ログ/監視、再試行、メンテナンス、アクセシブルな復旧導線
この記事で学べること(要点)
- Laravelの例外処理(
Handler、reportable/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;
}
}
- これを
webとapiに適用すると、調査が一気に楽になります。 - 画面側にも「お問い合わせの際はこの番号をお伝えください」と出せます。
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・キュー・混雑は“待てば直る”設計にし、ユーザーに状況を分かりやすく伝えます。
- アクセシビリティは、障害時ほど効きます。落ち着いて操作できる案内を標準にしましょう。
