例外チェーンで原因を握りつぶさない

例外チェーンは、例外を投げ直すときに第3引数previousへ元の例外を渡し、本当の原因を連鎖として残す書き方です。抽象度の高い例外で包みつつ、原因を失いません。

catchして投げ直すと原因が消える

低レイヤの例外をキャッチして、業務的に分かりやすい例外へ投げ直すのはよくある設計です。ところがこのとき元の例外を捨ててしまうと、本当の原因(スタックトレースやメッセージ)が失われて、ログを見ても「何かが失敗した」としか分からなくなります。デバッグでいちばん困るパターンですね。

try {
    $pdo->query($sql);
} catch (PDOException $e) {
    // これだと元の SQL エラーが消える
    throw new RuntimeException('ユーザー取得に失敗しました');
}

抽象度の高い例外に置き換えること自体は良いのですが、原因までは捨てないようにしたいところです。

第3引数previousに元例外を渡す

例外の第3引数 previous に元の例外を渡すと、原因がチェーンとしてつながります。第2引数はコード番号なので、そこは 0 を置いて第3引数に $e を入れる形になります。こうしておけば、業務例外を投げつつ低レイヤの詳細も失いません。

try {
    $pdo->query($sql);
} catch (PDOException $e) {
    throw new RuntimeException(
        'ユーザー取得に失敗しました',
        0,      // コード
        $e      // previous: これが原因の連鎖になる
    );
}

引数の順番(メッセージ, コード, previous)は忘れがちで、うっかり第2引数に $e を渡すと型エラーになります。ここは定型として覚えてしまうのが早いですね。

getPreviousで辿る、ログには全部出す

受け取った側は getPrevious() で原因をたどれます。ログには連鎖全体を出しておくと、後から追いやすいです。PHP の例外は文字列化すると previous も含めて出力してくれるので、まずはそのまま出すだけでも十分役に立ちます。

try {
    $service->fetchUser($id);
} catch (RuntimeException $e) {
    error_log((string)$e); // previous も連鎖して出る

    // 個別に辿ることもできる
    $cause = $e->getPrevious();
    if ($cause) {
        error_log('原因: ' . $cause->getMessage());
    }
}

まとめ

例外を投げ直すときは、第3引数 previous に元の例外を渡して原因を残すのが鉄則です。これを怠るとログに本当の失敗理由が出ず、調査が一気に難しくなります。抽象度の高い例外で包みつつ、原因はチェーンでつなぐ。受け取り側は文字列化やログ出力で連鎖ごと記録しておく。これだけで障害調査の速さがかなり変わると思います。

よくある質問

Q. 元の例外はどの引数に渡しますか。
A. 第3引数のpreviousです。第2引数はコード番号なので0を置き、第3引数に元例外を渡します。

Q. 連鎖した原因はどう取り出しますか。
A. getPrevious() で辿れます。例外を文字列化するとpreviousも含めて出力されるので、ログにはそのまま出すだけでも役立ちます。

Q. 低レイヤの例外をそのまま投げてはだめですか。
A. 抽象度の高い例外で包むのは良い設計ですが、その際previousで原因を残さないとログから本当の失敗理由が消えてしまいます。

類似投稿

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です