例外チェーンで原因を握りつぶさない
例外チェーンは、例外を投げ直すときに第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で原因を残さないとログから本当の失敗理由が消えてしまいます。