Skip to content

try / catch / finally

概述

try/catch/finally 是 PHP 异常处理的核心语法结构。try 块包含可能抛出异常的代码,catch 块捕获并处理特定类型的异常,finally 块无论是否发生异常都会执行,通常用于资源清理。

PHP 7.0+ 中 catch 支持同时捕获多种异常类型(用 | 分隔),PHP 8.0+ 中异常处理的性能和语法进一步优化。

版本说明

  • PHP 5.0 引入 try/catch
  • PHP 5.5 引入 finally
  • PHP 7.1 支持多异常类型捕获(catch (A | B $e)
  • PHP 8.0+ 异常不再自动转换为 Error

基础概念

基本结构

php
<?php

declare(strict_types=1);

try {
    // 可能抛出异常的代码
    $result = riskyOperation();
} catch (SpecificException $e) {
    // 处理特定异常
} catch (Throwable $e) {
    // 兜底处理所有异常
} finally {
    // 无论是否异常都执行
    // 用于资源释放、日志记录等
}

各部分说明

关键字作用是否必需
try包含可能抛出异常的代码必需
catch捕获并处理异常至少一个(或 finally)
finally始终执行的清理代码可选

语法与代码

基本 try/catch

php
<?php

declare(strict_types=1);

function divide(int $a, int $b): float
{
    if ($b === 0) {
        throw new InvalidArgumentException('除数不能为零');
    }
    return $a / $b;
}

try {
    echo divide(10, 2);  // 5
    echo divide(10, 0);  // 抛出异常
    echo '这行不会执行';  // 异常后不会执行
} catch (InvalidArgumentException $e) {
    echo "捕获到异常: " . $e->getMessage();  // 捕获到异常: 除数不能为零
}

多重 catch

php
<?php

declare(strict_types=1);

function processFile(string $path): string
{
    if (!file_exists($path)) {
        throw new FileNotFoundException("文件不存在: {$path}");
    }
    if (!is_readable($path)) {
        throw new RuntimeException("文件不可读: {$path}");
    }
    return file_get_contents($path);
}

try {
    $content = processFile('/some/file.txt');
} catch (FileNotFoundException $e) {
    echo "文件未找到: " . $e->getMessage();
} catch (RuntimeException $e) {
    echo "运行时错误: " . $e->getMessage();
} catch (Throwable $e) {
    echo "未知错误: " . $e->getMessage();
}

finally 始终执行

php
<?php

declare(strict_types=1);

function readFileContent(string $path): ?string
{
    $handle = null;
    try {
        $handle = fopen($path, 'r');
        if ($handle === false) {
            throw new RuntimeException("无法打开文件");
        }
        return stream_get_contents($handle);
    } catch (RuntimeException $e) {
        echo "错误: " . $e->getMessage();
        return null;
    } finally {
        // 无论是否发生异常都会执行
        if ($handle !== null) {
            fclose($handle);
        }
        echo "资源已释放\n";
    }
}

详细说明

finally 的执行时机

php
<?php

declare(strict_types=1);

// 情况 1:无异常 — finally 在 try 后执行
try {
    echo "try\n";
} catch (Exception $e) {
    echo "catch\n";
} finally {
    echo "finally\n";  // 始终执行
}
// 输出: try → finally

// 情况 2:有异常被捕获
try {
    echo "try\n";
    throw new Exception('error');
} catch (Exception $e) {
    echo "catch\n";
} finally {
    echo "finally\n";
}
// 输出: try → catch → finally

// 情况 3:异常未被捕获 — finally 执行后异常继续传播
function testUncaught(): void
{
    try {
        echo "try\n";
        throw new RuntimeException('uncaught');
    } finally {
        echo "finally\n";  // 仍会执行
    }
    echo "这行不会执行\n";
}

try {
    testUncaught();
} catch (RuntimeException $e) {
    echo "outer catch\n";
}
// 输出: try → finally → outer catch

// 情况 4:try 中有 return
function testReturn(): string
{
    try {
        return "try return";
    } finally {
        echo "finally (return 后仍执行)\n";
    }
}

echo testReturn();
// 输出: finally (return 后仍执行) → try return

finally 与 return/throw

finally 块会在 try/catch 块中的 returnthrowbreak 语句之前执行。finally 中的 return 会覆盖 try 中的 return

嵌套 try/catch

php
<?php

declare(strict_types=1);

function outerOperation(): void
{
    try {
        echo "外层 try\n";
        innerOperation();
        echo "外层 try 正常完成\n";
    } catch (RuntimeException $e) {
        echo "外层捕获: " . $e->getMessage() . "\n";
    }
}

function innerOperation(): void
{
    try {
        echo "内层 try\n";
        throw new InvalidArgumentException('内层错误');
    } catch (InvalidArgumentException $e) {
        echo "内层捕获: " . $e->getMessage() . "\n";
        // 可以选择重新抛出
        throw new RuntimeException('包装后的错误', 0, $e);
    }
}

outerOperation();
// 外层 try
// 内层 try
// 内层捕获: 内层错误
// 外层捕获: 包装后的错误

异常在循环中的处理

php
<?php

declare(strict_types=1);

$urls = [
    'https://example.com/api/1',
    'https://example.com/api/2',
    'https://invalid-url',
    'https://example.com/api/4',
];

$results = [];

foreach ($urls as $url) {
    try {
        // 模拟 HTTP 请求
        if (!str_starts_with($url, 'https://')) {
            throw new RuntimeException("无效的 URL: {$url}");
        }
        $results[$url] = ['status' => 'success'];
    } catch (RuntimeException $e) {
        $results[$url] = ['status' => 'error', 'message' => $e->getMessage()];
        // continue 到下一个 URL
    }
}

print_r($results);
// 3 个成功,1 个失败(循环继续)

异常信息获取

php
<?php

declare(strict_types=1);

function demonstrateExceptionInfo(): void
{
    try {
        throw new RuntimeException('演示异常信息', 1001);
    } catch (RuntimeException $e) {
        echo "类名: " . get_class($e) . "\n";
        echo "消息: " . $e->getMessage() . "\n";
        echo "错误码: " . $e->getCode() . "\n";
        echo "文件: " . $e->getFile() . "\n";
        echo "行号: " . $e->getLine() . "\n";

        // 堆栈跟踪(数组形式)
        $trace = $e->getTrace();
        echo "调用深度: " . count($trace) . "\n";

        // 堆栈跟踪(字符串形式)
        echo "堆栈:\n" . $e->getTraceAsString();

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

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

实战示例

数据库事务中的异常处理

php
<?php

declare(strict_types=1);

class DatabaseService
{
    public function transferFunds(int $fromId, int $toId, int $amount): void
    {
        $db = $this->getConnection();

        try {
            $db->beginTransaction();

            $this->debit($db, $fromId, $amount);
            $this->credit($db, $toId, $amount);

            $db->commit();
        } catch (PDOException $e) {
            $db->rollBack();
            throw new RuntimeException("转账失败: " . $e->getMessage(), 0, $e);
        } catch (InvalidArgumentException $e) {
            $db->rollBack();
            throw $e;
        } finally {
            // 确保连接关闭
            $db = null;
        }
    }

    private function debit(object $db, int $userId, int $amount): void
    {
        if ($amount <= 0) {
            throw new InvalidArgumentException('转账金额必须为正数');
        }
        // SQL: UPDATE accounts SET balance = balance - ? WHERE id = ?
    }

    private function credit(object $db, int $userId, int $amount): void
    {
        // SQL: UPDATE accounts SET balance = balance + ? WHERE id = ?
    }

    private function getConnection(): object
    {
        return new stdClass();  // 模拟
    }
}

注意事项

catch 顺序:从具体到通用

php
<?php

declare(strict_types=1);

// 错误顺序:RuntimeException 在前会捕获所有运行时异常
// try {
//     throw new InvalidArgumentException('test');
// } catch (RuntimeException $e) {  // 会捕获 InvalidArgumentException
//     echo "Runtime";
// } catch (InvalidArgumentException $e) {  // 永远不会执行
//     echo "Invalid";
// }

// 正确顺序:从子类到父类
try {
    throw new InvalidArgumentException('test');
} catch (InvalidArgumentException $e) {
    echo "Invalid: " . $e->getMessage();  // 先匹配
} catch (RuntimeException $e) {
    echo "Runtime: " . $e->getMessage();
} catch (Throwable $e) {
    echo "Throwable";
}

finally 中的 return 覆盖问题

php
<?php

declare(strict_types=1);

// finally 中的 return 会覆盖 try/catch 中的 return
function bad(): int
{
    try {
        return 1;
    } finally {
        return 2;  // 覆盖了 try 的 return!
    }
}
echo bad();  // 2(不是 1!)

// 正确做法:不在 finally 中使用 return
function good(): int
{
    $result = 0;
    try {
        $result = 1;
    } finally {
        // 资源清理,不 return
    }
    return $result;  // 1
}

finally 中不要使用 return

finally 块中的 return 语句会覆盖 trycatch 块中的 return。这是一个常见的 bug 来源,应避免在 finally 中使用 return

最佳实践

  1. catch 顺序从具体到通用:子类异常在前,父类在后
  2. finally 用于资源释放:文件句柄、数据库连接、锁等
  3. 异常消息要有意义:包含足够的上下文信息
  4. 避免空的 catch 块:至少记录日志
  5. 不要用异常控制流程:异常处理不是普通的控制流
php
<?php

declare(strict_types=1);

// 最佳实践模板
function processOrder(int $orderId): array
{
    try {
        $order = $this->findOrder($orderId);
        $this->validateOrder($order);
        $this->updateOrder($order);
        return ['success' => true, 'order' => $order];
    } catch (NotFoundException $e) {
        return ['success' => false, 'error' => '订单不存在'];
    } catch (ValidationException $e) {
        return ['success' => false, 'error' => $e->getMessage()];
    } catch (Throwable $e) {
        error_log($e->__toString());
        return ['success' => false, 'error' => '系统错误'];
    }
}

参考链接