> ## 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>

## 例外的回報

例外的回報是指將例外記錄到日誌，或送到 [Laravel Nightwatch](https://nightwatch.laravel.com)、[Sentry](https://github.com/getsentry/sentry-laravel)、[Flare](https://flareapp.io) 等外部服務。
預設會依據 `config/logging.php` 的設定寫入日誌。

### 自訂回報 callback

若想依例外種類做不同的回報處理，可透過 `report` 方法傳入閉包。
Laravel 會依閉包的型別提示判斷例外種類。

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

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->report(function (InvalidOrderException $e) {
        // 送到外部服務等
    });
})
```

註冊自訂 callback 後仍會保留預設的日誌記錄。
若要停止傳遞給預設處理，可呼叫 `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()` 可以在不中斷使用者回應的情況下記錄錯誤。適用於背景 job 或非重要處理的例外處理。
</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` 方法。
若能取得目前使用者 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 會自動產生合適的回應，但也可以自訂。

### 自訂渲染 callback

透過 `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()` 方法，Laravel 會自動呼叫，不必額外在 `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 或 Policy 一起搭配使用。
</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）、Origin 不一致（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-Hant">
<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  # 4xx 系列的備援
        └── 5xx.blade.php  # 5xx 系列的備援
```

<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-TW/packages/laravel-google-sheets/oauth.md)
- [Laravel Fetch Metadata](/zh-TW/packages/laravel-fetch-metadata.md)
- [用 Inertia.js 建構 SPA](/zh-TW/blog/inertia-introduction.md)
- [Laravel Pulse](/zh-TW/pulse.md)
- [Agent Loop](/zh-TW/packages/laravel-copilot-sdk/agent-loop.md)
