PHP 异常处理详解

小飞兽 PHP 26 次阅读 2026-04-30

概述

PHP 的异常处理机制为开发者提供了一种结构化的错误管理方式。与传统的错误报告(如 trigger_error)不同,异常机制允许代码主动抛出(throw)异常对象,调用者可以捕获(catch)并处理这些异常,从而实现错误的逐层传递和针对性处理。PHP 5 开始引入异常,到 PHP 7/8 已经非常成熟,是现代 PHP 开发的基础技能。

基础语法

PHP 异常处理的核心是 try、catch、finally 三个关键字的组合。try 块中放置可能抛出异常的代码,catch 块负责捕获并处理特定类型的异常,finally 块无论是否发生异常都会执行(常用于资源释放)。

    • throw:手动抛出一个异常,可以是任何实现了 Throwable 接口的对象。
    • try:包裹可能抛出异常的代码段。
    • catch:捕获指定类型的异常,处理逻辑写在这里。
    • finally:无论是否异常,都执行的代码块。
    • Exception:PHP 内置的异常基类,所有自定义异常应继承自此类。

    PHP 7 引入了多个捕获(multi-catch)语法,允许一个 catch 块处理多种异常类型。PHP 8 增加了 throw 表达式语法,可以在更紧凑的上下文中抛出异常。

    完整代码示例+注释

    <?php
    // === 自定义异常类 ===
    class ValidationException extends Exception {
        // 可以添加额外的字段和方法
        private array $errors;
    

    public function __construct(string $message, array $errors = [], int $code = 0, ?Throwable $previous = null) {
    parent::__construct($message, $code, $previous);
    $this->errors = $errors;
    }

    public function getErrors(): array {
    return $this->errors;
    }
    }

    // === 业务逻辑:用户注册验证 ===
    function registerUser(array $data): array {
    $errors = [];

    // 验证用户名
    if (empty($data['username'])) {
    $errors[] = '用户名不能为空';
    } elseif (strlen($data['username']) < 3) {
    $errors[] = '用户名至少需要3个字符';
    }

    // 验证邮箱格式
    if (empty($data['email']) || !filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
    $errors[] = '邮箱格式不正确';
    }

    // 验证密码强度
    if (empty($data['password']) || strlen($data['password']) < 8) {
    $errors[] = '密码至少需要8个字符';
    }

    // 如果存在验证错误,抛出自定义异常
    if (!empty($errors)) {
    throw new ValidationException('注册信息验证失败', $errors, 400);
    }

    // 验证通过,返回用户数据
    return [
    'id' => uniqid('user_'),
    'username' => $data['username'],
    'email' => $data['email'],
    'created_at' => date('Y-m-d H:i:s')
    ];
    }

    // === 调用并处理异常 ===
    try {
    $userData = [
    'username' => 'ab', // 故意使用过短的用户名
    'email' => 'not-an-email', // 故意使用无效邮箱
    'password' => 'weak' // 故意使用弱密码
    ];

    $result = registerUser($userData);
    echo "注册成功,用户ID: " . $result['id'] . PHP_EOL;

    } catch (ValidationException $e) {
    // 处理验证异常,显示所有错误信息
    echo "验证失败 (" . $e->getCode() . "): " . $e->getMessage() . PHP_EOL;
    foreach ($e->getErrors() as $error) {
    echo " - " . $error . PHP_EOL;
    }
    // 记录详细日志供排查
    error_log("Validation error: " . json_encode($e->getErrors()));

    } catch (Exception $e) {
    // 捕获其他所有未知异常
    echo "系统错误: " . $e->getMessage() . PHP_EOL;
    // 生产环境不要暴露详细错误给用户
    error_log($e->getTraceAsString());

    } finally {
    // 无论成功还是失败,最后都会执行这里
    // 适合关闭文件句柄、释放锁、记录日志等清理工作
    echo "注册流程结束。" . PHP_EOL;
    }

    // === 多异常捕获(PHP 7+)===
    try {
    $result = someRiskyOperation();
    } catch (ValidationException | DatabaseException $e) {
    // 同时捕获多种异常类型
    handleClientError($e);
    } catch (Throwable $e) {
    // 兜底捕获:任何可能的异常都不会漏掉
    handleServerError($e);
    }

    运行效果

    执行上述代码,由于注册数据中包含了不合规的字段,会触发 ValidationException 异常。输出结果为:

    验证失败 (400): 注册信息验证失败
    
  • 用户名至少需要3个字符
  • 邮箱格式不正确
  • 密码至少需要8个字符
注册流程结束。

可以看到,catch 块捕获了 ValidationException 并输出了完整的错误列表。finally 块始终执行,打印了"注册流程结束"。如果去掉自定义异常中的错误数组,只抛出普通 Exception,那么 catch 块中调用 getErrors() 时会报错(因为基类 Exception 没有该方法),这就体现了自定义异常的优势——携带更多上下文信息。

常见问题

Q1:什么时候应该用异常,什么时候用传统错误处理
异常适用于可以预见的、调用者需要知道的错误(如参数验证失败、文件不存在、数据库连接断开),这些错误需要调用者决定如何处理。传统错误处理(如 trigger_error)适用于真正意外的、程序级别的错误(如段错误、内存不足),通常由全局处理器统一记录日志而不是逐层处理。

Q2:catch 中不处理异常而是重新 throw 合适吗
合适,这叫异常重新抛出。常见场景是:在 catch 中先记录日志或做一些清理工作,然后重新 throw 让上层处理。例如连接数据库失败时先关闭已打开的连接句柄,再 throw 让调用者决定是否重试。

Q3:PHP 8 的 mixed 类型参数如何做类型检查
对于无法用类型声明精确定义的参数(mixed 类型),可以在函数开头用 assert() 做运行时断言,或手动判断类型后抛出 TypeError。PHP 8 的内部函数在类型不匹配时已经会自动抛出 TypeError。

Q4:异常会导致内存泄漏吗
在 try 块中创建的大对象如果被异常打断且没有及时释放,可能造成临时性内存占用上升。建议在 finally 中显式释放资源(关闭文件句柄、断开数据库连接等),或在 PHP 7.4+ 使用 WeakMap 管理缓存引用。

延伸阅读

    • PSR-18 HTTP Client 标准:描述了 HTTP 客户端如何通过异常传递错误,有助于构建统一的错误处理层级。
    • Whoops 库:PHP 错误处理库,提供漂亮的错误页面和完整的调用栈,特别适合开发阶段调试。
  • Sentry for PHP:错误追踪平台,自动收集生产环境的异常堆栈,帮助快速定位和修复线上问题。