新建一个 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 选项控制错误信息展示的详细程度。
默认情况下,它会读取 .env 中 APP_DEBUG 环境变量的值。
# 本地开发
APP_DEBUG=true
# 生产环境
APP_DEBUG=false
在生产环境中请务必将 APP_DEBUG 设为 false。若保持 true,可能会向最终用户暴露敏感信息。
异常的报告
异常报告指的是将异常记录到日志,或发送到 Sentry、Flare 等外部服务。
默认情况下会根据 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 中配置,它们也会被自动调用。
创建异常类
创建异常类
php artisan make:exception InvalidOrderException
实现 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.php 与 5xx.blade.php 作为兜底。
resources/
└── views/
└── errors/
├── 4xx.blade.php # 400 系列的兜底
└── 5xx.blade.php # 500 系列的兜底
Laravel 已经为 404、500、503 提供了默认页面。如果要定制这些,请创建单独文件(例如 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 错误响应格式