> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 错误处理

> 了解 Laravel 的异常处理机制，涵盖异常的报告、渲染、自定义错误页面以及 HTTP 错误响应的生成。

## 概述

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

### 异常处理流程

下面展示了从异常抛出到响应返回给客户端的过程。

```mermaid theme={null}
flowchart TD
    A["抛出异常"] --> B["ExceptionHandler::report"]
    B --> C{"是否需要记录日志?"}
    C -->|"是"| D["记录日志"]
    C -->|"否"| E["跳过"]
    D --> F["ExceptionHandler::render"]
    E --> F
    F --> G{"请求类型"}
    G -->|"Web"| H["HTML 错误页面"]
    G -->|"API/JSON"| I["JSON 错误响应"]
    H --> J["返回给客户端"]
    I --> J
```

```php theme={null}
// 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` 环境变量的值。

```ini theme={null}
# 本地开发
APP_DEBUG=true

# 生产环境
APP_DEBUG=false
```

<Warning>
  在生产环境中请务必将 `APP_DEBUG` 设为 `false`。若保持 `true`，可能会向最终用户暴露敏感信息。
</Warning>

## 异常的报告

异常报告指的是将异常记录到日志，或发送到 [Sentry](https://github.com/getsentry/sentry-laravel)、[Flare](https://flareapp.io) 等外部服务。
默认情况下会根据 `config/logging.php` 的设置写入日志。

### 自定义报告回调

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

```php theme={null}
use App\Exceptions\InvalidOrderException;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->report(function (InvalidOrderException $e) {
        // 通知外部服务等
    });
})
```

即便注册了自定义回调，默认的日志记录仍会继续。
如果要阻止向默认处理传播，可以调用 `stop()`，或返回 `false`。

```php theme={null}
->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->report(function (InvalidOrderException $e) {
        // ...
    })->stop();
})
```

### `report()` 辅助函数

如果只想报告异常而不显示错误页面，可以使用 `report()` 辅助函数。

```php theme={null}
public function isValid(string $value): bool
{
    try {
        // 校验处理...
    } catch (Throwable $e) {
        report($e);

        return false;
    }
}
```

<Tip>
  `report()` 辅助函数可以在不中断用户响应的情况下记录错误，非常适合后台任务或不重要处理中的异常处理。
</Tip>

### 防止重复报告

如果同一个异常实例多次被传给 `report()`，日志中可能会出现重复条目。
设置 `dontReportDuplicates()` 后，同一个实例只会在第一次时被记录。

```php theme={null}
->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->dontReportDuplicates();
})
```

```php theme={null}
$original = new RuntimeException('Whoops!');

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

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

### 全局日志上下文

若要给所有异常日志添加公共信息，可以使用 `context` 方法。
如果可用，Laravel 会自动附加当前用户 ID。

```php theme={null}
->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->context(fn () => [
        'app_version' => config('app.version'),
    ]);
})
```

### 给异常类添加 `context()` 方法

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

```php theme={null}
<?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` 方法。

```php theme={null}
use PDOException;
use Psr\Log\LogLevel;

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

### 异常报告的节流

出现大量异常时，可以通过 `throttle` 方法控制报告数量。

```php theme={null}
use Illuminate\Support\Lottery;
use Throwable;

->withExceptions(function (Exceptions $exceptions): void {
    // 大约每 1000 次随机报告 1 次
    $exceptions->throttle(function (Throwable $e) {
        return Lottery::odds(1, 1000);
    });
})
```

若要按每分钟条数限制，可以使用 `Limit`。

```php theme={null}
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` 方法传入闭包即可把异常转换为响应。

```php theme={null}
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`）的渲染也可以被覆盖。
如果闭包没有返回值，就会退回默认渲染。

```php theme={null}
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`。

```php theme={null}
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` 方法可以对生成的响应进行进一步处理。

```php theme={null}
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` 中配置，它们也会被自动调用。

### 创建异常类

<Steps>
  <Step title="创建异常类">
    ```shell theme={null}
    php artisan make:exception InvalidOrderException
    ```
  </Step>

  <Step title="实现 report() 与 render()">
    ```php theme={null}
    <?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);
        }
    }
    ```
  </Step>
</Steps>

<Info>
  `report()` 方法可以通过类型提示使用依赖注入，Laravel 的服务容器会自动解析。
</Info>

### `ShouldntReport` 接口

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

```php theme={null}
<?php

namespace App\Exceptions;

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

class PodcastProcessingException extends Exception implements ShouldntReport
{
    //
}
```

## 抛出异常

### `abort()` 辅助函数

可以在应用的任何位置抛出 HTTP 错误响应。

```mermaid theme={null}
flowchart LR
    A["abort(404)"] --> B["NotFoundHttpException"]
    C["abort(403)"] --> D["AccessDeniedHttpException"]
    E["abort(500)"] --> F["HttpException<br>500"]
    G["abort(422)"] --> H["UnprocessableEntityHttpException"]
    B --> I["resources/views/errors/404.blade.php"]
    D --> J["resources/views/errors/403.blade.php"]
    F --> K["resources/views/errors/500.blade.php"]
    H --> L["resources/views/errors/422.blade.php"]
```

```php theme={null}
// 404 Not Found
abort(404);

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

### `abort_if()` / `abort_unless()`

用于按条件抛出异常的辅助函数。

```php theme={null}
// 当 $condition 为 true 时 abort
abort_if(! $user->isAdmin(), 403);

// 当 $condition 为 false 时 abort
abort_unless($user->hasPermission('edit'), 403, 'Permission denied.');
```

<Tip>
  在控制器或中间件中做权限检查时非常方便，也常与 Gate 或策略搭配使用。
</Tip>

## 异常的全局控制

### 忽略特定异常

使用 `dontReport` 指定不上报的异常。自定义的渲染逻辑仍然生效。

```php theme={null}
use App\Exceptions\InvalidOrderException;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->dontReport([
        InvalidOrderException::class,
    ]);
})
```

如果要按条件忽略，可以给 `dontReportWhen` 传入闭包。

```php theme={null}
use Throwable;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->dontReportWhen(function (Throwable $e) {
        return $e instanceof PodcastProcessingException &&
               $e->reason() === 'Subscription expired';
    });
})
```

<Info>
  Laravel 默认会自动忽略部分异常，比如 404、CSRF Token 无效 (419)、来源不一致 (403) 等。
</Info>

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

要让默认被忽略的异常重新参与报告，可以使用 `stopIgnoring`。

```php theme={null}
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` 变量访问错误信息。

```blade theme={null}
{{-- 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` 获取。

```shell theme={null}
php artisan vendor:publish --tag=laravel-errors
```

### 兜底错误页面

对于没有对应状态码视图的情况，可以创建 `4xx.blade.php` 与 `5xx.blade.php` 作为兜底。

```
resources/
└── views/
    └── errors/
        ├── 4xx.blade.php  # 400 系列的兜底
        └── 5xx.blade.php  # 500 系列的兜底
```

<Warning>
  Laravel 已经为 `404`、`500`、`503` 提供了默认页面。如果要定制这些，请创建单独文件（例如 `404.blade.php`）而非依赖兜底。
</Warning>

## 实战示例：API 异常处理器

在提供 API 的应用中，异常始终需要以 JSON 返回。
下面是一个在 `bootstrap/app.php` 中集中管理 API 错误的示例实现。

```php theme={null}
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 theme={null}
<?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 theme={null}
<?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);
    }
}
```

## 小结

<AccordionGroup>
  <Accordion title="异常报告要点">
    | 方法                       | 用途              |
    | ------------------------ | --------------- |
    | `$exceptions->report()`  | 按异常类型注册自定义报告逻辑  |
    | `$exceptions->context()` | 给所有异常日志添加公共信息   |
    | `context()` 方法           | 让异常类携带自己特有的上下文  |
    | `report()` 辅助函数          | 不中断响应，只上报异常     |
    | `dontReportDuplicates()` | 防止同一实例被重复上报     |
    | `ShouldntReport` 接口      | 创建一个完全不会被上报的异常类 |
  </Accordion>

  <Accordion title="异常渲染要点">
    | 方法                       | 用途                  |
    | ------------------------ | ------------------- |
    | `$exceptions->render()`  | 按异常类型返回自定义响应        |
    | `render()` 方法            | 让异常类携带自己的渲染逻辑       |
    | `shouldRenderJsonWhen()` | 自定义 JSON/HTML 的判断逻辑 |
    | `respond()`              | 对生成的响应进行二次加工        |
  </Accordion>

  <Accordion title="HTTP 错误页要点">
    * 只需创建如 `resources/views/errors/404.blade.php` 之类的文件即可自动生效
    * 视图内可通过 `$exception` 变量访问错误详情
    * 可通过 `php artisan vendor:publish --tag=laravel-errors` 获取默认模板
    * 通过 `4xx.blade.php` / `5xx.blade.php` 可定义兜底错误页
  </Accordion>

  <Accordion title="生产环境的最佳实践">
    * 一定要设置 `APP_DEBUG=false`，避免把堆栈信息暴露给用户
    * 接入 Sentry、Flare 等外部错误追踪服务，集中管理错误
    * 使用 `throttle()` 避免大量异常时日志被淹没
    * API 接口保持一致的 JSON 错误响应格式
  </Accordion>
</AccordionGroup>


## Related topics

- [OAuth 2.0 认证 - Google Sheets API for Laravel](/zh/packages/laravel-google-sheets/oauth.md)
- [Laravel Fetch Metadata](/zh/packages/laravel-fetch-metadata.md)
- [使用 Inertia.js 构建 SPA](/zh/blog/inertia-introduction.md)
- [Laravel Pennant](/zh/pennant.md)
- [Laravel Pulse](/zh/pulse.md)
