PHP Exception

try/catch/finally, custom exceptions, exception chaining, and when to throw.

Intermediate
Key PointsExceptions separate the failure from its handling, keeping the happy path clean.

Basic structure

PHP
<?php
function divide(float $a, float $b): float {
    if ($b == 0.0) {
        throw new InvalidArgumentException("除数不能为 0");
    }
    return $a / $b;
}

try {
    echo divide(10, 4), "\n";
    echo divide(10, 0), "\n";
    echo "这行不会执行\n";
} catch (InvalidArgumentException $e) {
    echo "捕获: " . $e->getMessage(), "\n";
} finally {
    echo "finally 总会执行\n";
}
Output
2.5
捕获: 除数不能为 0
finally 总会执行

After throw the function ends immediately and control passes to the nearest matching catch. finally always runs — good for cleanup.

Multiple catch blocks

catch blocks match top-down, so put subclasses before parents. Leading with Exception disables everything below.

PHP
<?php
class ValidationException extends Exception {}
class NotFoundException extends Exception {}

function run(string $kind): void {
    match ($kind) {
        "v" => throw new ValidationException("字段非法"),
        "n" => throw new NotFoundException("记录不存在"),
        default => throw new RuntimeException("未知错误"),
    };
}

foreach (["v", "n", "x"] as $k) {
    try {
        run($k);
    } catch (ValidationException $e) {
        echo "校验失败: " . $e->getMessage() . "\n";
    } catch (NotFoundException $e) {
        echo "找不到: " . $e->getMessage() . "\n";
    } catch (Exception $e) {
        echo "其它: " . $e->getMessage() . "\n";
    }
}
Output
校验失败: 字段非法
找不到: 记录不存在
其它: 未知错误

Custom exceptions

Custom exceptions can carry structured data instead of concatenated strings.

PHP
<?php
class ValidationException extends Exception
{
    public function __construct(private array $errors)
    {
        parent::__construct("校验失败: " . implode(", ", $errors));
    }

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

try {
    throw new ValidationException(["name 不能为空", "email 格式错误"]);
} catch (ValidationException $e) {
    echo $e->getMessage(), "\n";
    print_r($e->errors());
    echo $e->getCode(), "\n";
}
Output
校验失败: name 不能为空, email 格式错误
Array
(
    [0] => name 不能为空
    [1] => email 格式错误
)
0

getCode() defaults to 0; pass a business code as the second constructor argument. Note Exception::$code must be an int.

Exception chaining

Never swallow a low-level exception — wrap it and pass it as previous to keep the chain.

PHP
<?php
class RepositoryException extends RuntimeException {}

function findUser(int $id): array {
    try {
        if ($id !== 1) {
            throw new PDOException("SQLSTATE[HY000] 连接超时");
        }
        return ["id" => 1, "name" => "Tom"];
    } catch (PDOException $e) {
        throw new RepositoryException("查询用户失败: id=$id", 0, $e);
    }
}

try {
    findUser(2);
} catch (RepositoryException $e) {
    echo $e->getMessage(), "\n";
    echo "原因: " . $e->getPrevious()->getMessage(), "\n";
}
Output
查询用户失败: id=2
原因: SQLSTATE[HY000] 连接超时
The third argument is previous. Without it the caller only sees "query failed" — never the real cause.

Throw or return

SituationApproach
调用方无法继续(依赖缺失、配置错误)抛异常
可预期的业务分支(找不到、余额不足)抛异常或返回结果对象
用户输入校验收集错误数组,不抛
「没有更多数据」返回空数组 / null
程序 bug(不可能的状态)抛 LogicException

A practical rule: exceptions for the shouldn't-happen, return values for normal branches. Using exceptions for control flow forces try everywhere.

Never swallow

PHP
<?php
// 反例:吞掉异常,问题被掩盖
try {
    throw new RuntimeException("磁盘写满");
} catch (Exception $e) {
    // 什么都不做
}
echo "反例:异常被吞,程序以为一切正常\n";

// 正例:记录后重新抛出,或转成业务异常
try {
    throw new RuntimeException("磁盘写满");
} catch (Exception $e) {
    error_log("写入失败: " . $e->getMessage());
    echo "正例:已记录\n";
}
Output
反例:异常被吞,程序以为一切正常
正例:已记录
catch (Exception $e) {} deserves a review rejection. Handle it, log and rethrow, or don't catch. Empty catches turn bugs into "intermittent, cannot reproduce".

Friends & Partners