Skip to content

Throwable 接口

概述

Throwable 是 PHP 异常和错误体系的顶层接口,是 ErrorException 的共同父接口。从 PHP 7.0 开始,所有的异常和错误都实现了 Throwable 接口,使得 catch (Throwable $e) 能够捕获所有可抛出的对象。

Throwable 不能被用户自定义的类直接实现(PHP 内部保留),用户只能通过继承 ExceptionError 来创建自定义的异常/错误类型。

版本说明

Throwable 接口在 PHP 7.0 中引入。PHP 5.x 中的 ExceptionError 没有共同父接口。PHP 8.0+ 中 Throwable 的地位更加重要,因为所有错误默认都转为异常抛出。

基础概念

接口定义

php
<?php

declare(strict_types=1);

// Throwable 接口定义(简化)
// interface Throwable extends Stringable
// {
//     public function getMessage(): string;
//     public function getCode(): int;
//     public function getFile(): string;
//     public function getLine(): int;
//     public function getTrace(): array;
//     public function getTraceAsString(): string;
//     public function getPrevious(): ?Throwable;
//     public function __toString(): string;
// }

Throwable 层次结构

Throwable
├── Error(PHP 内部错误)
│   ├── TypeError
│   ├── ValueError
│   ├── ArgumentCountError
│   ├── ArithmeticError
│   │   └── DivisionByZeroError
│   ├── CompileError
│   │   └── ParseError
│   ├── UnhandledMatchError
│   └── FiberError

└── Exception(用户异常)
    ├── ErrorException
    ├── LogicException
    │   ├── InvalidArgumentException
    │   ├── OutOfBoundsException
    │   ├── OutOfRangeException
    │   ├── RuntimeException
    │   ├── OverflowException
    │   ├── RangeException
    │   ├── UnderflowException
    │   ├── UnexpectedValueException
    │   └── DomainException
    └── RuntimeException
        ├── PDOException
        ├── RuntimeException 子类...
        └── ...

语法与代码

catch(Throwable) 捕获所有

php
<?php

declare(strict_types=1);

function riskyOperation(): string
{
    $result = 1 / 0;  // 抛出 DivisionByZeroError
    return "操作成功";
}

try {
    riskyOperation();
} catch (Throwable $e) {
    echo "捕获到 " . get_class($e) . "\n";
    echo "消息: {$e->getMessage()}\n";
    echo "文件: {$e->getFile()}:{$e->getLine()}\n";
}

// 输出:
// 捕获到 DivisionByZeroError
// 消息: Division by zero
// 文件: /path/to/file.php:8

不能被用户类直接实现

php
<?php

declare(strict_types=1);

// 编译错误!不能直接实现 Throwable
// class MyThrowable implements Throwable
// {
//     public function getMessage(): string {}
//     public function getCode(): int {}
//     public function getFile(): string {}
//     public function getLine(): int {}
//     public function getTrace(): array {}
//     public function getTraceAsString(): string {}
//     public function getPrevious(): ?Throwable {}
//     public function __toString(): string {}
// }
// Fatal error: Class MyThrowable cannot implement interface Throwable

// 正确方式:继承 Exception 或 Error
class AppException extends Exception
{
    // 可以添加自定义属性和方法
}

class AppError extends Error
{
    // 继承 Error 很少使用,一般用 Exception
}

Throwable 的方法

php
<?php

declare(strict_types=1);

function demonstrateThrowableMethods(): void
{
    try {
        $array = [1, 2, 3];
        echo $array[10];  // 越界访问不会抛异常(PHP 8.1 之前)
        // 但类型错误会抛异常
        strlen([]);        // TypeError
    } catch (Throwable $e) {
        echo "类名: " . get_class($e) . "\n";
        echo "消息: " . $e->getMessage() . "\n";
        echo "错误码: " . $e->getCode() . "\n";
        echo "文件: " . $e->getFile() . "\n";
        echo "行号: " . $e->getLine() . "\n";

        echo "堆栈跟踪:\n";
        echo $e->getTraceAsString();

        // 之前的异常(异常链)
        $previous = $e->getPrevious();
        if ($previous !== null) {
            echo "前一个异常: " . $previous->getMessage() . "\n";
        }

        // 字符串表示(Stringable)
        echo $e->__toString();
    }
}

demonstrateThrowableMethods();

详细说明

PHP 7 前后的区别

php
<?php

declare(strict_types=1);

// PHP 5.x 中:错误和异常是两套完全不同的机制
// - Error 是错误,不能被 try/catch 捕获(需要 set_error_handler)
// - Exception 是异常,可以被 try/catch 捕获

// PHP 7.0+ 中:统一为 Throwable
// - Error 实现 Throwable,可以被 try/catch 捕获
// - Exception 实现 Throwable,可以被 try/catch 捕获

// PHP 7.0+ 兼容方案
function globalErrorHandler(int $errno, string $errstr, string $errfile, int $errline): bool
{
    // 将 Error 转为 ErrorException 以便统一处理
    throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
}

set_error_handler('globalErrorHandler');

Error 和 Exception 的区别

php
<?php

declare(strict_types=1);

// Error:PHP 内部错误,通常不应被捕获和忽略
// - 表示程序存在严重问题
// - 捕获后通常需要记录日志并终止程序
// - 例如:TypeError, ParseError, DivisionByZeroError

// Exception:业务异常,表示可预期的异常情况
// - 表示程序遇到了可处理的异常情况
// - 捕获后可以恢复程序执行
// - 例如:InvalidArgumentException, RuntimeException

function divide(int $a, int $b): float
{
    if ($b === 0) {
        // 业务异常:使用 Exception
        throw new InvalidArgumentException('除数不能为零');
    }

    return $a / $b;
}

try {
    echo divide(10, 0);
} catch (InvalidArgumentException $e) {
    // 处理业务异常
    echo "业务错误: {$e->getMessage()}\n";
} catch (Throwable $e) {
    // 兜底捕获
    echo "未知错误: {$e->getMessage()}\n";
    throw $e;  // 非 Exception 错误应继续抛出
}

异常链(Previous Exception)

php
<?php

declare(strict_types=1);

class UserService
{
    /**
     * @throws RuntimeException
     */
    public function createUser(string $email, string $name): int
    {
        try {
            $this->validateEmail($email);
            return $this->insertUser($email, $name);
        } catch (InvalidArgumentException $e) {
            // 将验证异常包装为业务异常,保留原始异常
            throw new RuntimeException(
                "创建用户失败: {$e->getMessage()}",
                0,
                $e  // 传入 previous 异常
            );
        }
    }

    private function validateEmail(string $email): void
    {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException("邮箱格式无效: {$email}");
        }
    }

    private function insertUser(string $email, string $name): int
    {
        // 模拟数据库插入
        return 1;
    }
}

try {
    $service = new UserService();
    $service->createUser('invalid-email', 'Test');
} catch (RuntimeException $e) {
    echo "外层异常: {$e->getMessage()}\n";

    // 遍历异常链
    $current = $e;
    $depth = 0;
    while ($current !== null) {
        $indent = str_repeat('  ', $depth);
        echo "{$indent}→ " . get_class($current) . ": {$current->getMessage()}\n";
        $current = $current->getPrevious();
        $depth++;
    }
}

实战示例

统一异常处理器

php
<?php

declare(strict_types=1);

class ExceptionHandler
{
    public function handle(Throwable $e): void
    {
        $isError = $e instanceof Error;
        $level = $isError ? 'ERROR' : 'EXCEPTION';

        $this->log($level, $e);

        if ($isError) {
            // Error 通常需要终止程序
            $this->renderErrorPage($e);
            exit(1);
        }

        // Exception 可以恢复
        $this->renderExceptionPage($e);
    }

    private function log(string $level, Throwable $e): void
    {
        $message = sprintf(
            "[%s] [%s] %s in %s:%d\nStack Trace:\n%s",
            date('Y-m-d H:i:s'),
            $level,
            $e->getMessage(),
            $e->getFile(),
            $e->getLine(),
            $e->getTraceAsString()
        );

        error_log($message);
    }

    private function renderErrorPage(Throwable $e): void
    {
        http_response_code(500);
        echo "服务器内部错误";
    }

    private function renderExceptionPage(Throwable $e): void
    {
        http_response_code(400);
        echo "请求错误: {$e->getMessage()}";
    }
}

// 注册全局异常处理器
set_exception_handler(function (Throwable $e): void {
    $handler = new ExceptionHandler();
    $handler->handle($e);
});

注意事项

不要捕获所有 Throwable 后忽略

php
<?php

declare(strict_types=1);

// 反面示例:捕获所有异常后忽略
function badPractice(): mixed
{
    try {
        return riskyCalculation();
    } catch (Throwable $e) {
        // 危险!捕获了所有异常和错误,包括 OutOfMemoryError
        return null;
    }
}

// 正面示例:区分 Error 和 Exception
function goodPractice(): mixed
{
    try {
        return riskyCalculation();
    } catch (InvalidArgumentException $e) {
        // 处理特定的业务异常
        return null;
    } catch (RuntimeException $e) {
        // 处理运行时异常
        throw new RuntimeException("操作失败", 0, $e);
    }
    // 不捕获 Error,让它向上传播
}

getPrevious() 的使用

php
<?php

declare(strict_types=1);

// 注意:getPrevious() 返回的是构造函数中传入的第三个参数
// 如果使用 throw new Exception('msg', 0, $previous),则 getPrevious() 返回 $previous
// 如果只是 throw new Exception('msg'),则 getPrevious() 返回 null

try {
    throw new Exception('第三层异常');
} catch (Exception $e3) {
    try {
        throw new RuntimeException('第二层异常', 0, $e3);
    } catch (RuntimeException $e2) {
        throw new RuntimeException('第一层异常', 0, $e2);
    }
} catch (Throwable $e) {
    // 遍历异常链
    do {
        echo get_class($e) . ": " . $e->getMessage() . "\n";
    } while (($e = $e->getPrevious()) !== null);
}

最佳实践

  1. 仅在顶层使用 catch(Throwable):在框架/应用的全局异常处理器中使用,业务代码应捕获具体异常类型
  2. 区分 Error 和 Exception:Error 表示程序级错误,Exception 表示业务级异常
  3. 保持异常链完整:使用 new Exception('msg', 0, $previous) 保留原始异常信息
  4. Error 通常不应被忽略:捕获 Error 后应记录日志并终止或重新抛出
  5. 自定义异常继承 Exception:不要继承 Error,除非明确需要创建自定义错误类型
php
<?php

declare(strict_types=1);

// 全局异常处理器模板(推荐放在入口文件)
set_exception_handler(function (Throwable $throwable): void {
    $context = [
        'type'    => get_class($throwable),
        'message' => $throwable->getMessage(),
        'code'    => $throwable->getCode(),
        'file'    => $throwable->getFile(),
        'line'    => $throwable->getLine(),
        'trace'   => $throwable->getTraceAsString(),
    ];

    // 记录日志
    error_log(json_encode($context, JSON_UNESCAPED_UNICODE));

    // 区分环境返回不同响应
    if (PHP_SAPI === 'cli') {
        fwrite(STDERR, "错误: {$throwable->getMessage()}\n");
        exit(1);
    }

    http_response_code(500);
    echo json_encode(['error' => '服务器内部错误']);
});

参考链接