例外の握りつぶしをやめる、空catchとcatch(\Exception)の乱用
例外の握りつぶしとは、catchで例外を捕らえながら何もせず失敗を隠してしまうことです。痕跡が消えてデバッグ不能になる、避けたいアンチパターンです。
空のcatchは未来の自分への攻撃
とりあえずエラーを消したくて catch を空にする、というのは誰しも一度は通る道だと思います。ただこれ、問題が起きても何も起きなかったように振る舞うので、後からデバッグするときに手がかりが完全に消えるんですよね。バグが静かに埋まる、一番たちの悪いパターンです。
try {
$result = $api->fetch();
} catch (Exception $e) {
// 何もしない ← 失敗が闇に消える
}
// $result は未定義のまま先へ進む
この後で $result を使えば未定義通知が出ますし、出なくても本来あるべきデータが欠けたまま処理が続きます。原因が握りつぶした場所と離れるので、追跡が地獄になります。
catch(\Exception)で全部拾うと想定外まで飲み込む
もう一つよくあるのが、広すぎる catch です。\Exception や \Throwable で一括して拾うと、本来キャッチしたかったネットワークエラーだけでなく、コードのバグ由来の例外まで一緒に飲み込んでしまいます。結果、プログラミングミスが「正常系のリトライ」として処理されたりするわけですね。
try {
$user = $repository->find($id);
$mailer->send($user->emial); // タイポ → Error
} catch (\Throwable $e) {
return null; // バグまで null に化けて気づけない
}
拾うなら型を絞り、握るなら記録する
基本は「回復できる特定の例外だけを、その型で拾う」だと思います。どうしても広く拾う必要があるなら、せめてログに残して痕跡を消さないことですね。握りつぶすにしても、後から追える形にしておくかどうかで雲泥の差になります。
try {
$result = $api->fetch();
} catch (ConnectionException $e) {
// 回復可能な失敗だけを扱う
$logger->warning('取得失敗、既定値を使用', ['error' => $e->getMessage()]);
$result = $default;
}
まとめ
例外を消すこと自体が悪いのではなく、痕跡ごと消すのが問題なんだと思います。拾う型はできるだけ具体的に、想定外は上まで投げて落とす、どうしても握るならログを残す。この三つを意識するだけで、原因不明のバグに夜中まで付き合わされる回数がだいぶ減る気がします。
よくある質問
Q. 空のcatchはなぜ悪いのですか?
A. 失敗が何も起きなかったように振る舞い、痕跡が消えるからです。原因が握りつぶした場所と離れて追跡が困難になります。
Q. catch(\Throwable)で全部拾うのは?
A. 回復したいエラーだけでなくコードのバグ由来の例外まで飲み込むので、想定外を上まで投げられなくなります。
Q. どうしても広く拾う必要があるときは?
A. せめてログに残して痕跡を消さないことです。後から追える形にしておくかどうかで大きく変わります。