跳转到主要内容

概述

新建一个 Laravel 项目时,错误与异常的处理都是开箱即用的。 自定义配置可以通过 bootstrap/app.php 中的 withExceptions 方法进行。

异常处理流程

下面展示了从异常抛出到响应返回给客户端的过程。
// bootstrap/app.php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;

return Application::configure(basePath: dirname(__DIR__))
    ->withExceptions(function (Exceptions $exceptions): void {
        // 在这里设置异常的报告与渲染
    })->create();
withExceptions 闭包中的 $exceptions 对象是 Illuminate\Foundation\Configuration\Exceptions 的实例,负责管理整个应用的异常处理。

调试设置

config/app.php 中的 debug 选项控制错误信息展示的详细程度。 默认情况下,它会读取 .envAPP_DEBUG 环境变量的值。
# 本地开发
APP_DEBUG=true

# 生产环境
APP_DEBUG=false
在生产环境中请务必将 APP_DEBUG 设为 false。若保持 true,可能会向最终用户暴露敏感信息。

异常的报告

异常报告指的是将异常记录到日志,或发送到 SentryFlare 等外部服务。 默认情况下会根据 config/logging.php 的设置写入日志。

自定义报告回调

若希望不同类型的异常有不同的报告逻辑,可以给 report 方法传入一个闭包。 Laravel 会根据闭包的类型提示判断适用的异常类型。
use App\Exceptions\InvalidOrderException;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->report(function (InvalidOrderException $e) {
        // 通知外部服务等
    });
})
即便注册了自定义回调,默认的日志记录仍会继续。 如果要阻止向默认处理传播,可以调用 stop(),或返回 false
->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->report(function (InvalidOrderException $e) {
        // ...
    })->stop();
})

report() 辅助函数

如果只想报告异常而不显示错误页面,可以使用 report() 辅助函数。
public function isValid(string $value): bool
{
    try {
        // 校验处理...
    } catch (Throwable $e) {
        report($e);

        return false;
    }
}
report() 辅助函数可以在不中断用户响应的情况下记录错误,非常适合后台任务或不重要处理中的异常处理。

防止重复报告

如果同一个异常实例多次被传给 report(),日志中可能会出现重复条目。 设置 dontReportDuplicates() 后,同一个实例只会在第一次时被记录。
->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->dontReportDuplicates();
})
$original = new RuntimeException('Whoops!');

report($original); // 会被记录

try {
    throw $original;
} catch (Throwable $caught) {
    report($caught); // 会被忽略(同一实例)
}

全局日志上下文

若要给所有异常日志添加公共信息,可以使用 context 方法。 如果可用,Laravel 会自动附加当前用户 ID。
->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->context(fn () => [
        'app_version' => config('app.version'),
    ]);
})

给异常类添加 context() 方法

在异常类自身上定义 context() 方法,就能在日志中加入该异常特有的上下文信息。
<?php

namespace App\Exceptions;

use Exception;

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

    /**
     * 返回异常的上下文信息。
     *
     * @return array<string, mixed>
     */
    public function context(): array
    {
        return ['order_id' => $this->orderId];
    }
}

修改日志级别

若要以特定日志级别记录某个异常,可以使用 level 方法。
use PDOException;
use Psr\Log\LogLevel;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->level(PDOException::class, LogLevel::CRITICAL);
})

异常报告的节流

出现大量异常时,可以通过 throttle 方法控制报告数量。
use Illuminate\Support\Lottery;
use Throwable;

->withExceptions(function (Exceptions $exceptions): void {
    // 大约每 1000 次随机报告 1 次
    $exceptions->throttle(function (Throwable $e) {
        return Lottery::odds(1, 1000);
    });
})
若要按每分钟条数限制,可以使用 Limit
use Illuminate\Broadcasting\BroadcastException;
use Illuminate\Cache\RateLimiting\Limit;
use Throwable;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->throttle(function (Throwable $e) {
        if ($e instanceof BroadcastException) {
            return Limit::perMinute(300);
        }
    });
})

异常的渲染

渲染就是把异常转换为 HTTP 响应的过程。 默认情况下 Laravel 会自动生成合适的响应,但也可以自定义。

自定义渲染回调

render 方法传入闭包即可把异常转换为响应。
use App\Exceptions\InvalidOrderException;
use Illuminate\Http\Request;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->render(function (InvalidOrderException $e, Request $request) {
        return response()->view('errors.invalid-order', status: 500);
    });
})
内置异常(如 NotFoundHttpException)的渲染也可以被覆盖。 如果闭包没有返回值,就会退回默认渲染。
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->render(function (NotFoundHttpException $e, Request $request) {
        if ($request->is('api/*')) {
            return response()->json([
                'message' => 'Record not found.',
            ], 404);
        }
        // 返回 null 时将显示默认 404 页面
    });
})

自动判断 JSON / HTML

Laravel 会根据请求的 Accept 头自动判断返回 HTML 还是 JSON。 如果需要自定义这一逻辑,可以使用 shouldRenderJsonWhen
use Illuminate\Http\Request;
use Throwable;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) {
        if ($request->is('admin/*')) {
            return true; // 管理后台始终返回 JSON
        }

        return $request->expectsJson();
    });
})

自定义整个响应

使用 respond 方法可以对生成的响应进行进一步处理。
use Symfony\Component\HttpFoundation\Response;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->respond(function (Response $response) {
        if ($response->getStatusCode() === 419) {
            return back()->with([
                'message' => '页面已过期,请重试。',
            ]);
        }

        return $response;
    });
})

自定义异常类

你可以在 app/Exceptions/ 目录下创建自己的异常类。 如果在异常类中定义了 report()render() 方法,即使不在 bootstrap/app.php 中配置,它们也会被自动调用。

创建异常类

1

创建异常类

php artisan make:exception InvalidOrderException
2

实现 report() 与 render()

<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

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

    /**
     * 报告异常。
     */
    public function report(): void
    {
        // 通知外部服务等
    }

    /**
     * 将异常转换为 HTTP 响应。
     */
    public function render(Request $request): Response
    {
        return response()->view('errors.invalid-order', [
            'orderId' => $this->orderId,
        ], 422);
    }
}
report() 方法可以通过类型提示使用依赖注入,Laravel 的服务容器会自动解析。

ShouldntReport 接口

对于不需要报告的异常,可以实现 ShouldntReport 接口。 实现了这个接口的异常将不会被报告。
<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Contracts\Debug\ShouldntReport;

class PodcastProcessingException extends Exception implements ShouldntReport
{
    //
}

抛出异常

abort() 辅助函数

可以在应用的任何位置抛出 HTTP 错误响应。
// 404 Not Found
abort(404);

// 带消息
abort(403, '你没有权限执行此操作。');

abort_if() / abort_unless()

用于按条件抛出异常的辅助函数。
// 当 $condition 为 true 时 abort
abort_if(! $user->isAdmin(), 403);

// 当 $condition 为 false 时 abort
abort_unless($user->hasPermission('edit'), 403, 'Permission denied.');
在控制器或中间件中做权限检查时非常方便,也常与 Gate 或策略搭配使用。

异常的全局控制

忽略特定异常

使用 dontReport 指定不上报的异常。自定义的渲染逻辑仍然生效。
use App\Exceptions\InvalidOrderException;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->dontReport([
        InvalidOrderException::class,
    ]);
})
如果要按条件忽略,可以给 dontReportWhen 传入闭包。
use Throwable;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->dontReportWhen(function (Throwable $e) {
        return $e instanceof PodcastProcessingException &&
               $e->reason() === 'Subscription expired';
    });
})
Laravel 默认会自动忽略部分异常,比如 404、CSRF Token 无效 (419)、来源不一致 (403) 等。

让 Laravel 恢复上报默认忽略的异常

要让默认被忽略的异常重新参与报告,可以使用 stopIgnoring
use Symfony\Component\HttpKernel\Exception\HttpException;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->stopIgnoring(HttpException::class);
})

HTTP 错误页面

Laravel 允许为不同的 HTTP 状态码定义自定义错误视图。

创建自定义错误视图

resources/views/errors/ 目录下,以状态码为文件名创建 Blade 模板。
resources/
└── views/
    └── errors/
        ├── 404.blade.php
        ├── 403.blade.php
        └── 500.blade.php
视图内可以通过 $exception 变量访问错误信息。
{{-- resources/views/errors/404.blade.php --}}
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>页面未找到</title>
</head>
<body>
    <h1>404 - 页面未找到</h1>
    <p>{{ $exception->getMessage() }}</p>
    <a href="{{ url('/') }}">回到首页</a>
</body>
</html>

发布默认错误模板

如果希望在 Laravel 自带的错误页面基础上定制,可以通过 vendor:publish 获取。
php artisan vendor:publish --tag=laravel-errors

兜底错误页面

对于没有对应状态码视图的情况,可以创建 4xx.blade.php5xx.blade.php 作为兜底。
resources/
└── views/
    └── errors/
        ├── 4xx.blade.php  # 400 系列的兜底
        └── 5xx.blade.php  # 500 系列的兜底
Laravel 已经为 404500503 提供了默认页面。如果要定制这些,请创建单独文件(例如 404.blade.php)而非依赖兜底。

实战示例:API 异常处理器

在提供 API 的应用中,异常始终需要以 JSON 返回。 下面是一个在 bootstrap/app.php 中集中管理 API 错误的示例实现。
use Illuminate\Auth\AuthenticationException;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

->withExceptions(function (Exceptions $exceptions): void {
    // API 请求始终返回 JSON
    $exceptions->render(function (NotFoundHttpException $e, Request $request) {
        if ($request->is('api/*')) {
            return response()->json([
                'message' => '资源未找到。',
            ], 404);
        }
    });

    $exceptions->render(function (AuthenticationException $e, Request $request) {
        if ($request->is('api/*')) {
            return response()->json([
                'message' => '需要认证。',
            ], 401);
        }
    });

    $exceptions->render(function (ValidationException $e, Request $request) {
        if ($request->is('api/*')) {
            return response()->json([
                'message' => '校验失败。',
                'errors'  => $e->errors(),
            ], 422);
        }
    });
})

自定义 API 异常类的实现

创建一个 API 专用的基础异常类,可以让每个接口返回一致的错误响应。
<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class ApiException extends Exception
{
    public function __construct(
        string $message = 'An error occurred.',
        private readonly int $statusCode = 500,
        private readonly array $errors = [],
    ) {
        parent::__construct($message);
    }

    public function render(Request $request): JsonResponse
    {
        $data = ['message' => $this->getMessage()];

        if (! empty($this->errors)) {
            $data['errors'] = $this->errors;
        }

        return response()->json($data, $this->statusCode);
    }
}
在控制器中使用示例:
<?php

namespace App\Http\Controllers\Api;

use App\Exceptions\ApiException;
use App\Models\Order;

class OrderController extends Controller
{
    public function show(int $id): JsonResponse
    {
        $order = Order::find($id);

        if (! $order) {
            throw new ApiException('订单不存在。', 404);
        }

        if ($order->isCancelled()) {
            throw new ApiException('该订单已被取消。', 422);
        }

        return response()->json($order);
    }
}

小结

方法用途
$exceptions->report()按异常类型注册自定义报告逻辑
$exceptions->context()给所有异常日志添加公共信息
context() 方法让异常类携带自己特有的上下文
report() 辅助函数不中断响应,只上报异常
dontReportDuplicates()防止同一实例被重复上报
ShouldntReport 接口创建一个完全不会被上报的异常类
方法用途
$exceptions->render()按异常类型返回自定义响应
render() 方法让异常类携带自己的渲染逻辑
shouldRenderJsonWhen()自定义 JSON/HTML 的判断逻辑
respond()对生成的响应进行二次加工
  • 只需创建如 resources/views/errors/404.blade.php 之类的文件即可自动生效
  • 视图内可通过 $exception 变量访问错误详情
  • 可通过 php artisan vendor:publish --tag=laravel-errors 获取默认模板
  • 通过 4xx.blade.php / 5xx.blade.php 可定义兜底错误页
  • 一定要设置 APP_DEBUG=false,避免把堆栈信息暴露给用户
  • 接入 Sentry、Flare 等外部错误追踪服务,集中管理错误
  • 使用 throttle() 避免大量异常时日志被淹没
  • API 接口保持一致的 JSON 错误响应格式
最后修改于 2026年7月13日