【PHP】Laravelでエラー処理(例外の報告・描画とカスタムエラーページ)を実装する方法

対象バージョン: Laravel 13.x / PHP 8.3+(laravel/framework v13.38.0 で動作確認)

Laravel のエラー処理は、例外を記録する「報告(report)」と、利用者に返すレスポンスを作る「描画(render)」の2つに分かれます。この記事は、Laravel で 404 ページを自作したい方や、例外をログにどう残すか整理したい方向けです。bootstrap/app.php の withExceptions の使い方、カスタムエラーページの作り方、JSON のエラーレスポンス、ログへの報告、テストまでを順に説明します。

この記事で分かること

  • 例外処理を設定する場所(bootstrap/app.php の withExceptions)と、最初から入っている設定
  • APP_DEBUG によるエラー表示の違いと、本番での注意点
  • abort() で HTTP エラーを返す方法
  • 404.blade.php などのカスタムエラーページと、4xx.blade.php / 5xx.blade.php が使われない8つのステータス
  • JSON のエラーレスポンスと、メッセージの差し替え
  • 例外の報告(ログ)の仕組みと調整方法
  • Exceptions::fake() を使ったテスト

前提

Laravel 13.x は PHP 8.3 以上が必要です。動作確認は、composer create-project laravel/laravel で新規作成したアプリ(laravel/laravel v13.11.0、laravel/framework v13.38.0、PHP 8.4)で行いました。プロジェクトの作成方法は【PHP】Laravel入門:インストールから簡単なサンプルアプリケーションの作成を参照してください。

例外処理の設定場所: bootstrap/app.php

Laravel 11 以降、新規アプリには app/Exceptions/Handler.php がありません。例外の報告と描画は、bootstrap/app.php の withExceptions に渡すクロージャで設定します。引数の $exceptions は Illuminate\Foundation\Configuration\Exceptions のインスタンスです。

新規作成した 13.x アプリの bootstrap/app.php は次のとおりです。

<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware): void {
        //
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        $exceptions->shouldRenderJsonWhen(
            fn (Request $request) => $request->is('api/*') || $request->expectsJson(),
        );
    })->create();

withExceptions のクロージャは空ではなく、shouldRenderJsonWhen の設定が最初から1つ入っています。この設定により、api/* のパスへのリクエストは、Accept: application/json ヘッダーが無くてもエラーが JSON で返ります。この行は laravel/laravel v13.9.0 以降のスケルトンに含まれています。それより前のスケルトンで作成したプロジェクトでは、内容が異なる(v13.8.0 は api/* の判定だけ、v13.7.0 以前は空のクロージャ)ことがあります。

この記事で追加する設定は、すべてこの既存の行の下に書きます。

APP_DEBUG とエラー表示

エラー表示の詳しさは config('app.debug') で決まり、この値は .env の APP_DEBUG に従います。

例外の種類 APP_DEBUG=true APP_DEBUG=false
HTTP 例外以外(RuntimeException など) 例外クラス、メッセージ、ファイルパスを含むデバッグページ 汎用の「Server Error」ページ(500)
HTTP 例外(abort()、存在しないルート、findOrFail() の失敗) エラービュー エラービュー

JSON の場合も同様です。APP_DEBUG=true では message に加えて exception、file、line、trace が含まれます。APP_DEBUG=false では、HTTP 例外以外は {"message": "Server Error"} だけになります。

デバッグページにはファイルパスなどアプリ内部の情報が表示されます。本番環境では APP_DEBUG を必ず false にしてください。

HTTP 例外は APP_DEBUG=true でもエラービューで表示されます。そのため、自作の 404 ページはローカル環境でもそのまま確認できます。

動作確認用のルートを用意する

この記事の例で使うルートです。routes/web.php を次の内容にします(/ のルートは新規作成時からあるものです)。例外クラスは後の節で作成します。

<?php

use App\Exceptions\InvalidOrderException;
use App\Exceptions\OrderNotReadyException;
use App\Models\User;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('welcome');
});

// abort() / abort_if() / abort_unless()
Route::get('/forbidden', function () {
    abort(403, 'この操作は許可されていません。');
});

Route::get('/abort-if', function () {
    abort_if(request()->boolean('deny'), 403);

    return 'allowed';
});

Route::get('/abort-unless', function () {
    abort_unless(request()->boolean('ok'), 404);

    return 'ok';
});

// findOrFail() で見つからなければ 404
Route::get('/users/{id}', function (int $id) {
    return User::findOrFail($id);
});

// キャッチされない例外
Route::get('/boom', function () {
    throw new RuntimeException('Something broke (demo).');
});

// 独自の例外
Route::get('/invalid-order', function () {
    throw new InvalidOrderException(42);
});

Route::get('/order-not-ready', function () {
    throw new OrderNotReadyException('Order 7 is not ready.');
});

// 例外をキャッチして報告だけする
Route::get('/report-helper', function () {
    try {
        throw new RuntimeException('report helper demo');
    } catch (Throwable $e) {
        report($e);
    }

    return 'continued after report()';
});

// 本物のバリデーション例外(abort(422) との比較用)
Route::get('/validate', function () {
    request()->validate(['name' => 'required']);

    return 'valid';
});

abort() で HTTP エラーを返す

abort($code, $message) は HTTP 例外(HttpException)を投げます。404 の場合は NotFoundHttpException です。条件付きで中断したいときは abort_if() / abort_unless() を使います。

呼び出し 結果
abort(403, 'この操作は許可されていません。') 403
abort_if(true, 403) 403
abort_if(false, 403) 中断せず処理を続ける
abort_unless(false, 404) 404
abort_unless(true, 404) 中断せず処理を続ける

上のルートでは、/abort-if?deny=1 が 403、/abort-unless が 404 になります。

投げられた例外はエラービューに $exception として渡されます。$exception->getMessage() で abort() に渡したメッセージを取得できます。メッセージを渡さずに abort(404) とした場合は空文字列です。

ミドルウェアの中で abort(403) を使う例は【PHP】Laravelでミドルウェアを実装する方法で扱っています。認可の実装は【PHP】Laravelで認証と認可を実装する方法、API のトークン認証は【PHP】Laravel SanctumでAPIトークン認証を実装する方法を参照してください。

カスタムエラーページを作る

404.blade.php を作る

resources/views/errors/ にステータスコード名のビューを置くと、そのステータスのエラーページとして使われます。404.blade.php は、次のすべての 404 で表示されます。

  • abort(404)
  • 存在しない URL へのアクセス
  • findOrFail() でレコードが見つからない場合(ModelNotFoundException)

まず、エラーページで使うレイアウトコンポーネント resources/views/components/layouts/app.blade.php を作ります。

@props(['title' => config('app.name')])
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>{{ $title }}</title>
</head>
<body>
    <main>
        {{ $slot }}
    </main>
</body>
</html>

次に resources/views/errors/404.blade.php を作ります。

<x-layouts.app title="ページが見つかりません">
    <h1>ページが見つかりません</h1>
    <p>お探しのページは見つかりませんでした。</p>
    <a href="{{ url('/') }}">トップへ戻る</a>
</x-layouts.app>

レイアウトコンポーネントの詳細は【PHP】LaravelでBladeコンポーネントとレイアウトを使う方法を参照してください。

ビューで $exception->getMessage() を表示する場合は注意が必要です。存在しない URL では「The route no-such-page could not be found.」、findOrFail() の失敗では「No query results for model [App\Models\User] 999999」のように、ルートやモデルのクラス名、ID を含むメッセージになります。abort(404) のようにメッセージが空の場合もあるため、表示するなら $exception->getMessage() ?: 'ページが見つかりません' のように代わりの文言を用意します。

4xx.blade.php / 5xx.blade.php と、使われない8つのステータス

個別のビューが無いステータスには、フォールバックとして 4xx.blade.php と 5xx.blade.php が使われます。たとえば resources/views/errors/4xx.blade.php は次のように書けます。

<!DOCTYPE html>
<html lang="ja">
<head><meta charset="utf-8"><title>エラー {{ $exception->getStatusCode() }}</title></head>
<body>
<h1>エラーが発生しました</h1>
<p>ステータス: {{ $exception->getStatusCode() }}</p>
</body>
</html>

5xx.blade.php も同じ形で作れます。

ただし、次の8つのステータスにはフォールバックが使われません。Laravel 本体がこれらのステータス用のビューを持っていて、4xx / 5xx より先にそちらが選ばれるためです。

ステータス 表示されるページ(Laravel 本体)
401 Unauthorized
402 Payment Required
403 Forbidden
404 Not Found
419 Page Expired
429 Too Many Requests
500 Server Error
503 Service Unavailable

4xx.blade.php と 5xx.blade.php だけを置いた状態では、次のように分かれます。

  • 4xx.blade.php が使われる: 400、405、408、410、418、422 など
  • 5xx.blade.php が使われる: 501、502、504 など
  • Laravel 本体のページのまま: 上の表の8つ

この8つのページを変えたい場合は、403.blade.php や 404.blade.php のように個別のビューを作ります。以前のドキュメントでは対象外を 404・500・503 の3つと説明していましたが、実際の動作は 11.x 以降どのバージョンでも上の8つです。13.x のドキュメントは8つに修正されています。

実際の例外の場合も、同じルールでページが選ばれます。

発生した状況 ステータス 使われるページ
存在しない URL、findOrFail() の失敗 404 404.blade.php(無ければ本体の Not Found)
CSRF トークンの無い POST 419 本体の Page Expired(419.blade.php を作れば差し替え可能)
許可されていない HTTP メソッド 405 4xx.blade.php
throttle ミドルウェアの制限超過 429 本体の Too Many Requests(429.blade.php を作れば差し替え可能)
キャッチされない例外(APP_DEBUG=false) 500 本体の Server Error(500.blade.php を作れば差し替え可能)

キャッチされない例外には 500.blade.php を作る

上の表のとおり、キャッチされない例外は 500 として描画され、5xx.blade.php ではなく Laravel 本体の「Server Error」ページになります。この画面を変えるには resources/views/errors/500.blade.php を作ります。

<!DOCTYPE html>
<html lang="ja">
<head><meta charset="utf-8"><title>サーバーエラー</title></head>
<body>
<h1>サーバーエラーが発生しました</h1>
<p>時間をおいて再度お試しください。</p>
</body>
</html>

Laravel 本体のエラービューを公開して編集する

Laravel 本体のエラービューをもとに編集したい場合は、次のコマンドでアプリにコピーできます。

php artisan vendor:publish --tag=laravel-errors

resources/views/errors/ に次の10ファイルがコピーされます。ただし、同名のファイルが既にある場合は上書きされないため、この記事の手順で作成した 404.blade.php と 500.blade.php はそのまま残ります。

  • 401.blade.php、402.blade.php、403.blade.php、404.blade.php、419.blade.php、429.blade.php、500.blade.php、503.blade.php
  • 共通レイアウトの layout.blade.php と minimal.blade.php

各ステータスのビューは @extends('errors::minimal') でレイアウトを継承しています。コピーしたビューを編集すると、本体のビューより優先して使われます。たとえば本体の 404.blade.php は次の内容です。

@extends('errors::minimal')

@section('title', __('Not Found'))
@section('code', '404')
@section('message', __('Not Found'))

403.blade.php は __($exception->getMessage() ?: 'Forbidden') を表示するため、abort(403, 'この操作は許可されていません。') のメッセージがそのまま画面に出ます。一方、本体の 404.blade.php は常に固定の「Not Found」を表示します。

JSON のエラーレスポンス

次のどちらかに当てはまるとき、Laravel はエラーを HTML ではなく JSON で返します。

  • $request->expectsJson() が true(Accept: application/json ヘッダーがあるなど)
  • shouldRenderJsonWhen のコールバックが true を返す(新規アプリでは api/* のパス)

APP_DEBUG=false でも、HTTP 例外のメッセージは JSON にそのまま含まれます。

リクエスト レスポンス(APP_DEBUG=false)
存在しない URL {"message": "The route no-such-page could not be found."}
abort(403, 'この操作は許可されていません。') {"message": "この操作は許可されていません。"}
findOrFail() の失敗 {"message": "No query results for model [App\\Models\\User] 999999"}
キャッチされない RuntimeException {"message": "Server Error"}

存在しない URL ではパスが、findOrFail() の失敗ではモデルのクラス名と ID がメッセージに含まれます。外部に見せたくない場合は、render コールバックでレスポンスを差し替えます。bootstrap/app.php の先頭の use 文に次の1行を追加します。

use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

そのうえで、withExceptions のクロージャ内、既存の shouldRenderJsonWhen の下に次を追加します。

$exceptions->render(function (NotFoundHttpException $e, Request $request) {
    if ($request->is('api/*')) {
        return response()->json([
            'message' => 'Record not found.',
        ], 404);
    }
});

この例では、api/* の 404 は {"message": "Record not found."} になります。api/* 以外のリクエストではコールバックが何も返さないため、通常どおり 404.blade.php が表示されます。

API の作り方は【PHP】LaravelでRESTful APIを作成する方法を参照してください。

例外を報告する(ログに残す)

既定の記録先と書式

新規アプリのログ設定(LOG_CHANNEL=stack、LOG_STACK=single)では、報告された例外は storage/logs/laravel.log に次の形式で書き込まれます(パスは短縮しています)。

[2026-10-23 01:20:08] local.ERROR: Something broke (demo). {"exception":"[object] (RuntimeException(code: 0): Something broke (demo). at <app>/routes/web.php:36)
[stacktrace]

先頭の日時は app.timezone の設定に従い、新規アプリでは UTC です。上の 01:20:08 は日本時間では 10:20:08 にあたります。

ログインしているユーザーがいる場合、Laravel はそのユーザーの ID を "userId" としてログに追加します。

HTTP 例外は既定では報告されない

次の例外は、既定ではログに記録されません。

  • HTTP 例外(abort(404)、abort(500) を含む)
  • findOrFail() の失敗による ModelNotFoundException
  • ValidationException、AuthenticationException、AuthorizationException、TokenMismatchException など

abort(500) も記録されない点に注意してください。HTTP 例外も記録したい場合は、bootstrap/app.php の先頭の use 文に次を追加します。

use Symfony\Component\HttpKernel\Exception\HttpException;

そのうえで、withExceptions のクロージャ内に次を追加します。

$exceptions->stopIgnoring(HttpException::class);

独自の例外クラスを作る

ここからの例で使う例外クラス app/Exceptions/InvalidOrderException.php を作ります。

<?php

namespace App\Exceptions;

use Exception;

class InvalidOrderException extends Exception
{
    public function __construct(public int $orderId, string $message = 'Invalid order.')
    {
        parent::__construct($message);
    }

    /**
     * Exception-specific log context.
     *
     * @return array<string, mixed>
     */
    public function context(): array
    {
        return ['order_id' => $this->orderId];
    }
}

context() メソッドが返す配列は、この例外のログにコンテキストとして追加されます。

report コールバックで報告処理を追加する

特定の例外を報告するときに処理を追加するには、$exceptions->report() にクロージャを渡します。どの例外に適用するかは、クロージャの引数の型で決まります。bootstrap/app.php の先頭の use 文に次を追加します。

use App\Exceptions\InvalidOrderException;
use Illuminate\Support\Facades\Log;

そのうえで、withExceptions のクロージャ内に次を追加します。

$exceptions->report(function (InvalidOrderException $e) {
    Log::info('custom report callback: InvalidOrderException order='.$e->orderId);
});

/invalid-order にアクセスすると、laravel.log にはコールバックの INFO と、Laravel の既定の ERROR の2行が書き込まれます。コールバックを登録しても、既定のログ記録は続けて行われます。

既定のログ記録を止めたい場合は、コールバックに ->stop() をつなげるか、コールバックから false を返します。

$exceptions->report(function (InvalidOrderException $e) {
    Log::info('custom report callback: InvalidOrderException order='.$e->orderId);
})->stop();

すべての例外ログに情報を追加する

$exceptions->context() を使うと、すべての例外ログに共通の情報を追加できます。withExceptions のクロージャ内に次を追加します。

$exceptions->context(fn () => [
    'app_ctx' => 'demo',
]);

この設定と InvalidOrderException の context() により、ログには {"order_id":42,"app_ctx":"demo","exception":...} のように両方の情報が含まれます。

report() ヘルパーで、処理を続けたまま報告する

例外をキャッチしたうえでログには残したい場合は、report() ヘルパーを使います。エラーページは表示されず、処理はそのまま続きます。前述の /report-helper ルートは 200 で「continued after report()」を返し、ログには ERROR が1件残ります。

report('メッセージ') のように文字列を渡すと、例外に包んで報告されます。条件付きで報告する report_if() / report_unless() もあります。

報告を調整する設定

withExceptions のクロージャ内では、次の設定もできます。必要なものだけ追加してください。例外クラス名はアプリに合わせて置き換えます。

目的 設定
特定の例外を報告しない(ページは通常どおり表示) $exceptions->dontReport([IgnoredException::class]);
例外の種類ごとにログレベルを変える(既定は error) $exceptions->level(CriticalLevelException::class, LogLevel::CRITICAL);
同じ例外インスタンスを1回だけ報告する $exceptions->dontReportDuplicates();
報告の回数を制限する $exceptions->throttle(...)(下の例)

level() を使う場合は use Psr\Log\LogLevel; を、throttle() の例では use Illuminate\Cache\RateLimiting\Limit; を bootstrap/app.php の先頭に追加します。表の例外クラスを自分のアプリのクラスに置き換えた場合は、そのクラスの use 文(例: use App\Exceptions\IgnoredException;)も bootstrap/app.php の先頭に追加します。

$exceptions->throttle(function (Throwable $e) {
    if ($e instanceof ThrottledException) {
        return Limit::perMinute(2);
    }
});

この例では、ThrottledException を5回報告しても、ログに残るのは1分あたり2件までです。

例外ごとにレスポンスを変える(render コールバック)

$exceptions->render() にクロージャを渡すと、特定の例外に対するレスポンスを自分で返せます。withExceptions のクロージャ内に次を追加します(InvalidOrderException の use 文は前の節で追加済みです)。

$exceptions->render(function (InvalidOrderException $e, Request $request) {
    if ($request->expectsJson()) {
        return response()->json([
            'message' => $e->getMessage(),
            'order_id' => $e->orderId,
        ], 400);
    }

    return response()->view('errors.invalid-order', [
        'orderId' => $e->orderId,
    ], 400);
});

ビュー resources/views/errors/invalid-order.blade.php を作ります。

<h1>注文が不正です</h1>
<p>注文番号: {{ $orderId }}</p>

/invalid-order は 400 でこのビューを返します。Accept: application/json を付けると {"message": "Invalid order.", "order_id": 42} が返ります。

例外クラスに report() / render() を定義する

例外クラス自身に report() と render() メソッドを定義することもできます。app/Exceptions/OrderNotReadyException.php を作ります。

<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;

class OrderNotReadyException extends Exception
{
    /**
     * Report the exception (void => default logging is skipped).
     */
    public function report(): void
    {
        Log::warning('OrderNotReadyException self-report: '.$this->getMessage());
    }

    /**
     * Render the exception as an HTTP response.
     */
    public function render(Request $request): Response
    {
        return response()->view('errors.order-not-ready', [
            'message' => $this->getMessage(),
        ], 409);
    }
}

ビュー resources/views/errors/order-not-ready.blade.php を作ります。

<h1>注文の準備ができていません</h1>
<p>{{ $message }}</p>

/order-not-ready は、render() が返したビューを 409 で返します。

ここで注意したいのが report() の戻り値です。例外クラスに report() があり、false 以外を返す場合(戻り値が void の場合も含む)、Laravel の既定のログ記録は行われません。上の例では、ログに残るのは report() 内で書いた WARNING の1行だけです。既定のログ記録も行いたい場合は、report() から false を返します。

public function report(): bool
{
    Log::info('ConditionalReportException custom part');

    return false;
}

abort(422) とバリデーションエラーは別物

abort(422) は単なる HTTP 例外で、4xx.blade.php があればそのページが表示されます。一方、$request->validate() が投げる本物の ValidationException はエラーページになりません。

  • 通常のリクエスト: 元のページへリダイレクト(302)
  • JSON のリクエスト: 422 と、errors を含む JSON

バリデーションエラーの扱いは【PHP】Laravelでフォームバリデーションを実装する方法を参照してください。

エラー処理をテストする

エラーページや JSON のエラーレスポンスは、Feature テストで確認できます。Exceptions::fake() を使うと、例外が報告されたかどうかも検証できます。フェイクに置き換えてもレスポンスは通常どおり描画され、dontReport の設定も反映されます。

tests/Feature/ErrorHandlingTest.php の例です。ここまでの節で作成したビュー、例外クラス、bootstrap/app.php の設定を前提にしています。

<?php

namespace Tests\Feature;

use App\Exceptions\InvalidOrderException;
use Illuminate\Support\Facades\Exceptions;
use RuntimeException;
use Tests\TestCase;

class ErrorHandlingTest extends TestCase
{
    public function test_custom_404_page_is_rendered(): void
    {
        $this->get('/no-such-page')
            ->assertStatus(404)
            ->assertSee('お探しのページは見つかりませんでした。');
    }

    public function test_framework_403_page_shows_abort_message(): void
    {
        $this->get('/forbidden')
            ->assertForbidden()
            ->assertSee('この操作は許可されていません。');
    }

    public function test_json_error_responses(): void
    {
        $this->getJson('/no-such-page')
            ->assertNotFound()
            ->assertJson(['message' => 'The route no-such-page could not be found.']);

        $this->getJson('/api/posts/1')
            ->assertNotFound()
            ->assertExactJson(['message' => 'Record not found.']);
    }

    public function test_real_validation_exception_is_not_abort_422(): void
    {
        $this->getJson('/validate')
            ->assertStatus(422)
            ->assertJsonValidationErrors('name');

        $this->get('/validate')->assertRedirect();
    }

    public function test_reported_exceptions_can_be_asserted_with_fake(): void
    {
        Exceptions::fake();

        $this->get('/report-helper')->assertOk()->assertSee('continued after report()');

        Exceptions::assertReported(RuntimeException::class);
        Exceptions::assertReported(fn (RuntimeException $e) => $e->getMessage() === 'report helper demo');
        Exceptions::assertReportedCount(1);
    }

    public function test_fake_still_renders_custom_response(): void
    {
        Exceptions::fake();

        $this->get('/invalid-order')->assertStatus(400)->assertSee('注文番号: 42');
        Exceptions::assertReported(fn (InvalidOrderException $e) => $e->orderId === 42);
    }
}

/api/posts/1 はルートを定義していない URL です。api/* のため JSON の 404 になり、前述の render コールバックのメッセージが返ります。報告されていないことを確かめるには Exceptions::assertNotReported(...) を使います。

テストの基本は【PHP】Laravelでテストを実装する方法を参照してください。

php artisan test

bootstrap/app.php の全体

この記事で追加した設定のうち、stopIgnoring と「報告を調整する設定」を除いたものをまとめると、bootstrap/app.php は次のようになります。

<?php

use App\Exceptions\InvalidOrderException;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware): void {
        //
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        // 新規作成時からある設定
        $exceptions->shouldRenderJsonWhen(
            fn (Request $request) => $request->is('api/*') || $request->expectsJson(),
        );

        $exceptions->report(function (InvalidOrderException $e) {
            Log::info('custom report callback: InvalidOrderException order='.$e->orderId);
        });

        $exceptions->context(fn () => [
            'app_ctx' => 'demo',
        ]);

        $exceptions->render(function (InvalidOrderException $e, Request $request) {
            if ($request->expectsJson()) {
                return response()->json([
                    'message' => $e->getMessage(),
                    'order_id' => $e->orderId,
                ], 400);
            }

            return response()->view('errors.invalid-order', [
                'orderId' => $e->orderId,
            ], 400);
        });

        $exceptions->render(function (NotFoundHttpException $e, Request $request) {
            if ($request->is('api/*')) {
                return response()->json([
                    'message' => 'Record not found.',
                ], 404);
            }
        });
    })->create();

よくあるつまずき

症状 原因と対処
4xx.blade.php を作ったのに 403 や 404 が変わらない 401・402・403・404・419・429・500・503 はフォールバックの対象外。403.blade.php などを個別に作る
5xx.blade.php を作ったのに例外の画面が変わらない キャッチされない例外は 500。500.blade.php を作る
api/* で Accept ヘッダーが無いのに JSON が返る 新規アプリの shouldRenderJsonWhen の設定による
JSON のエラーにモデルのクラス名や ID が出る HTTP 例外のメッセージは APP_DEBUG=false でも返る。render コールバックで差し替える
本番でエラーの詳細が表示される APP_DEBUG が true になっている。本番では必ず false にする
abort(500) や abort(404)・存在しない URL の 404 がログに残らない HTTP 例外は既定では報告されない。必要なら stopIgnoring(HttpException::class)。findOrFail() や暗黙のモデルバインディングによる 404(ModelNotFoundException)は、この設定でも記録されない
例外クラスに report() を書いたら既定のログが消えた false 以外(void を含む)を返すと既定のログ記録は行われない。残すなら false を返す
abort(422) でバリデーションエラーの表示にならない abort(422) は HTTP 例外。バリデーションは ValidationException で処理する
ログの時刻が9時間ずれている 新規アプリの app.timezone は UTC

Sentry などのエラー監視サービスとの連携や、ログチャンネルの詳しい設定はこの記事では扱いません。

まとめ

  • 例外の報告と描画は bootstrap/app.php の withExceptions で設定する。新規アプリには shouldRenderJsonWhen の設定が最初から入っている
  • 本番では APP_DEBUG を必ず false にする。HTTP 例外は APP_DEBUG に関係なくエラービューで表示される
  • resources/views/errors/404.blade.php などでステータスごとのページを作れる。4xx / 5xx のフォールバックは 401・402・403・404・419・429・500・503 には使われない
  • キャッチされない例外のページを変えるには 500.blade.php を作る
  • JSON では APP_DEBUG=false でも HTTP 例外のメッセージが返る。必要なら render コールバックで差し替える
  • HTTP 例外は既定ではログに残らない。例外クラスの report() が false 以外を返すと既定のログ記録は行われない
  • laravel.log の時刻は新規アプリでは UTC
  • abort(422) とバリデーションの ValidationException は別物
  • Exceptions::fake() で、報告された例外をテストで検証できる

関連記事

参考リンク

コメントする