Skip to content

Throwable 接口

概述

Throwable 是 PHP 7.0+ 引入的顶层异常接口,是所有可抛出对象(ErrorException)的共同父接口。它是 PHP 异常处理体系的核心,try/catch 语句只能捕获实现了 Throwable 接口的对象。

PHP 版本说明

  • Throwable 自 PHP 7.0 起引入
  • 它替代了 PHP 5.x 中只能 catch (\Exception $e) 的限制
  • PHP 7.0+ 的 try/catch 只能捕获 Throwable 实例
  • 用户自定义类不能直接 implements Throwable

基础概念

Throwable 的设计目的

在 PHP 5.x 中,try/catch 只能捕获 Exception,无法捕获 Fatal Error。PHP 7.0 引入了 Throwable 接口来统一错误和异常的处理:

Throwable (接口) — 不可被用户类直接实现
├── Error           — PHP 引擎内部错误
│   ├── TypeError
│   ├── ValueError (PHP 8.0+)
│   ├── ArgumentCountError
│   ├── ArithmeticError
│   │   └── DivisionByZeroError
│   ├── CompileError (PHP 7.3+)
│   │   └── ParseError
│   ├── UnhandledMatchError (PHP 8.0+)
│   └── FiberError (PHP 8.1+)
└── Exception       — 用户级异常
    ├── ErrorException
    ├── LogicException
    │   ├── InvalidArgumentException
    │   ├── OutOfBoundsException
    │   ├── RuntimeException
    │   ├── DomainException
    │   └── LengthException
    ├── RuntimeException
    │   ├── OutOfRangeException
    │   ├── OverflowException
    │   ├── RangeException
    │   ├── UnderflowException
    │   └── UnexpectedValueException
    └── ... (更多子类)

为什么用户类不能实现 Throwable

PHP 引擎内部通过特殊机制处理 Throwable,如果允许用户类直接实现,可能破坏错误处理的基本保证。因此 PHP 强制规定:

  • ErrorExceptionThrowable仅有的两个内置实现
  • 用户类只能 extends Exceptionextends Error(但不推荐后者)
  • 直接 implements Throwable 会导致编译错误
php
<?php

// 这行代码会导致编译错误:
// class MyError implements Throwable {}
// Fatal error: Class MyError cannot implement interface Throwable,
// it is implicitly implemented by Error and Exception

语法与代码

Throwable 接口定义

Throwable 接口定义了以下方法:

php
<?php

declare(strict_types=1);

// Throwable 接口的方法签名(内部定义)
interface 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;
}

// 无论捕获的是 Error 还是 Exception,都可以调用这些方法
function handleThrowable(\Throwable $e): void
{
    echo "消息: {$e->getMessage()}" . PHP_EOL;
    echo "代码: {$e->getCode()}" . PHP_EOL;
    echo "文件: {$e->getFile()}:{$e->getLine()}" . PHP_EOL;
    echo "前一个异常: " . ($e->getPrevious() ? $e->getPrevious()->getMessage() : '无') . PHP_EOL;
}

catch(Throwable $e) 捕获所有

php
<?php

declare(strict_types=1);

function riskyOperation(): mixed
{
    // 可能抛出 TypeError(Error 子类)
    $value = (int) 'not_a_number';

    // 可能抛出 Exception
    throw new \RuntimeException('操作失败');
}

try {
    riskyOperation();
} catch (\Throwable $e) {
    // 捕获所有 Error 和 Exception
    echo '捕获到 Throwable: ' . get_class($e) . PHP_EOL;
    echo '消息: ' . $e->getMessage() . PHP_EOL;
}

Throwable 类型声明

php
<?php

declare(strict_types=1);

// 函数参数使用 Throwable 类型声明
function logException(\Throwable $e, string $context = ''): void
{
    $logEntry = sprintf(
        "[%s] %s in %s:%d\n  %s",
        date('Y-m-d H:i:s'),
        $e->getMessage(),
        $e->getFile(),
        $e->getLine(),
        $e->getTraceAsString()
    );

    if ($context !== '') {
        $logEntry = "[{$context}] " . $logEntry;
    }

    error_log($logEntry);
}

// 可以传入 Error 或 Exception
try {
    $data = json_decode('{"invalid"', true, 512, JSON_THROW_ON_ERROR);
} catch (\Throwable $e) {
    logException($e, 'JSON_PARSE');
}

Throwable 与 finally 的配合

php
<?php

declare(strict_types=1);

function transactionalOperation(): string
{
    $connected = false;
    try {
        // 模拟连接数据库
        $connected = true;

        // 执行可能抛出 Error 或 Exception 的操作
        $result = intdiv(10, 0); // DivisionByZeroError

        return '操作成功';
    } catch (\Throwable $e) {
        echo "捕获异常: {$e->getMessage()}" . PHP_EOL;
        return '操作失败';
    } finally {
        // 无论是否抛出异常都会执行
        if ($connected) {
            echo '清理:关闭连接' . PHP_EOL;
        }
        echo 'finally 块执行完毕' . PHP_EOL;
    }
}

echo transactionalOperation();
// 输出:
// 捕获异常: Division by zero
// 清理:关闭连接
// finally 块执行完毕
// 操作失败

Throwable 在异常链中的使用

php
<?php

declare(strict_types=1);

class Application
{
    public static function run(): void
    {
        try {
            self::boot();
            self::handleRequest();
        } catch (\Throwable $e) {
            self::handleException($e);
        }
    }

    private static function boot(): void
    {
        // 模拟启动时加载配置
        $config = json_decode('bad json', true, 512, JSON_THROW_ON_ERROR);
    }

    private static function handleRequest(): void
    {
        echo '处理请求...';
    }

    private static function handleException(\Throwable $e): void
    {
        // 遍历完整异常链
        $chain = [];
        $current = $e;
        while ($current !== null) {
            $chain[] = sprintf(
                '[%s] %s in %s:%d',
                get_class($current),
                $current->getMessage(),
                basename($current->getFile()),
                $current->getLine()
            );
            $current = $current->getPrevious();
        }

        foreach ($chain as $i => $entry) {
            echo ($i + 1) . '. ' . $entry . PHP_EOL;
        }
    }
}

Application::run();

详细说明

Throwable 接口方法详解

方法返回类型说明示例
getMessage()string异常消息"Division by zero"
getCode()int异常代码1001
getFile()string异常发生的文件路径"/app/src/Service.php"
getLine()int异常发生的行号42
getTrace()array堆栈跟踪数组(结构化)[...]
getTraceAsString()string格式化的堆栈跟踪字符串"#0 ..."
getPrevious()?Throwable异常链中的前一个异常nullThrowable
__toString()string异常的字符串表示Exception: ...

getTrace() 返回的结构

php
<?php

declare(strict_types=1);

function inner(): void
{
    throw new \RuntimeException('内部错误');
}

function middle(): void
{
    inner();
}

function outer(): void
{
    middle();
}

try {
    outer();
} catch (\Throwable $e) {
    $trace = $e->getTrace();
    // $trace 是一个数组,每个元素代表一个调用栈帧:
    // [
    //   [
    //     'file' => '/path/to/file.php',
    //     'line' => 42,
    //     'function' => 'inner',
    //     'class' => null,          // 如果是静态方法则为类名
    //     'type' => null,           // '::' 或 '->'
    //     'args' => [],             // 函数参数
    //   ],
    //   ...
    // ]

    foreach ($trace as $i => $frame) {
        $func = $frame['class'] ?? '' . ($frame['type'] ?? '') . $frame['function'];
        $file = $frame['file'] ?? 'internal';
        $line = $frame['line'] ?? 0;
        echo "#{$i} {$func} in {$file}:{$line}" . PHP_EOL;
    }
}

PHP 5.x 与 PHP 7.0+ 的异常处理差异

特性PHP 5.xPHP 7.0+
Fatal Error 处理不可捕获,脚本终止转为 Error,可通过 catch 捕获
try/catch 捕获范围Exception所有 Throwable(Error + Exception)
统一捕获catch (\Throwable $e)
类型声明不支持 Throwable 参数类型支持 Throwable 类型声明
set_exception_handler只接收 Exception接收 Throwable

迁移注意

如果代码从 PHP 5.x 升级到 7.0+,某些原本导致 Fatal Error 的代码现在会抛出 Error。如果不捕获这些 Error,它们的行为与之前相同(未被捕获的 Throwable 会导致 Fatal Error)。

实战示例

全局异常处理器模板

php
<?php

declare(strict_types=1);

class GlobalExceptionHandler
{
    private const LOG_FILE = '/var/log/app/error.log';

    public static function register(): void
    {
        // 错误转 Throwable
        set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
            if (!(error_reporting() & $severity)) {
                return false;
            }
            throw new \ErrorException($message, 0, $severity, $file, $line);
        });

        // 全局 Throwable 处理
        set_exception_handler(function (\Throwable $e): void {
            self::log($e);
            self::respond($e);
        });

        // 致命错误兜底
        register_shutdown_function(function (): void {
            $error = error_get_last();
            if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR], true)) {
                self::logFatal($error);
            }
        });
    }

    private static function log(\Throwable $e): void
    {
        $entry = sprintf(
            "[%s] [%s] %s in %s:%d\n%s",
            date('Y-m-d H:i:s'),
            self::classify($e),
            $e->getMessage(),
            $e->getFile(),
            $e->getLine(),
            $e->getTraceAsString()
        );
        file_put_contents(self::LOG_FILE, $entry . "\n", FILE_APPEND | LOCK_EX);
    }

    private static function classify(\Throwable $e): string
    {
        return match (true) {
            $e instanceof \Error           => 'ERROR',
            $e instanceof \LogicException   => 'LOGIC',
            $e instanceof \RuntimeException  => 'RUNTIME',
            default                          => 'EXCEPTION',
        };
    }

    private static function respond(\Throwable $e): void
    {
        if (php_sapi_name() === 'cli') {
            fwrite(STDERR, "Error: {$e->getMessage()}\n");
            exit(1);
        }

        http_response_code(500);
        echo json_encode(['error' => '服务器内部错误', 'code' => $e->getCode()]);
    }

    private static function logFatal(array $error): void
    {
        $entry = sprintf(
            "[FATAL] %s in %s:%d",
            $error['message'],
            $error['file'],
            $error['line']
        );
        file_put_contents(self::LOG_FILE, $entry . "\n", FILE_APPEND | LOCK_EX);
    }
}

GlobalExceptionHandler::register();

注意事项

  1. catch (\Throwable $e) 会捕获所有:包括 ErrorException,用于最外层的兜底捕获。不要在业务逻辑内部滥用——应捕获具体的异常类型。

  2. Throwable 不能被用户类实现:如果需要自定义异常,必须 extends Exception 或其子类,不能 implements Throwable

  3. getTrace() 可能包含敏感数据:堆栈跟踪中的 args 字段会包含函数参数的实际值,可能包含密码、密钥等敏感信息。

  4. __toString() 可能抛出异常:如果在 __toString() 方法内部产生异常,会导致 Fatal Error。Throwable::__toString() 本身不会抛出异常。

  5. 未捕获的 Throwable 仍然是 Fatal Error:如果 Throwable 被抛出但未被捕获,PHP 仍然会产生 Fatal Error 并终止脚本。

最佳实践

推荐做法

  1. 外层用 Throwable,内层用具体类型:框架入口 catch (\Throwable $e),业务逻辑 catch (\InvalidArgumentException $e)
  2. 类型声明使用 Throwable:日志函数、异常处理器参数使用 \Throwable 类型声明
  3. 区分 Error 和 ExceptionError 通常需要修复代码,Exception 通常需要处理业务逻辑
  4. 生产环境过滤堆栈参数:记录日志前,清理 getTrace() 中的敏感参数
  5. 使用 set_exception_handler 处理未捕获异常:注册全局处理器,防止堆栈信息暴露给用户

参考链接