Aikido

catchブロックを空のままにするのではなく、エラーを処理すべき理由

可読性

ルール
ハンドル エラー  キャッチ ブロックでエラーを処理する。 
空の キャッチ ブロック 静かに エラーを飲み込む エラーを飲み込む、 
デバッグ デバッグ を難しくする。 
対応言語 Java、 C, C++, PHP、 JavaScript、 
TypeScript、 Go、 Python

はじめに

空のcatchブロックは、本番コードにおける最も危険なアンチパターンの一つです。例外が捕捉されても処理されない場合、エラーは痕跡を残さずに消滅します。アプリケーションは、実行を停止すべきであった破損した状態、無効なデータ、または失敗した操作のまま実行を継続します。ユーザーは、機能が動作しないサイレントな障害を目にしますが、エラーメッセージは受け取りません。運用チームはデバッグするためのログがありません。何かがおかしいという唯一の兆候は、連鎖的な障害によってシステムが使用不能になった数時間後、あるいは数日後に現れます。

なぜ重要なのか

デバッグとインシデント対応: 空のcatchブロックはエラーログを排除します。エンジニアはスタックトレース、エラーメッセージ、または障害が発生した時期や場所の兆候を持たず、問題の再現をほぼ不可能にします。

サイレントデータ破損: 空のcatchブロック内でデータベース操作やAPI呼び出しが失敗した場合、アプリケーションは成功したかのように動作を続けます。レコードは部分的に更新され、トランザクションは不完全になり、破損が発見される頃には監査証跡は失われています。

セキュリティ脆弱性: 空のcatchブロックは、認証エラーや認可チェックなどのセキュリティ障害を隠蔽します。セキュリティ上重要なパスで例外をトリガーした攻撃者は、エラーがサイレントに処理された場合、保護を完全にバイパスする可能性があります。

カスケード障害: エラーが無視されると、アプリケーションは無効な状態で続行されます。失敗した操作の結果に依存する後続の操作も失敗し、エンジニアを実際の根本原因から誤解させる一連の障害が発生します。

コード例

❌ 非準拠:

async function updateUserProfile(userId, profileData) {
    try {
        await db.users.update(userId, profileData);
        await cache.invalidate(`user:${userId}`);
        await searchIndex.update(userId, profileData);
    } catch (error) {
        // TODO: handle error
    }

    return { success: true };
}

誤っている理由: いずれかの操作が失敗した場合、エラーは黙って無視され、関数は成功を返します。データベースは更新されても、キャッシュの無効化が失敗し、古いデータが残る可能性があります。あるいは、検索インデックスの更新が失敗し、ユーザーが検索できなくなるにもかかわらず、問題を示すログやアラートがない状態になります。

✅ 準拠済み:

async function updateUserProfile(userId, profileData) {
    try {
        await db.users.update(userId, profileData);
        await cache.invalidate(`user:${userId}`);
        await searchIndex.update(userId, profileData);
        return { success: true };
    } catch (error) {
        logger.error('Failed to update user profile', {
            userId,
            error: error.message,
            stack: error.stack
        });
        throw new ProfileUpdateError(
            'Unable to update profile',
            { cause: error }
        );
    }
}

これが重要な理由:すべてのエラーはコンテキストとともにログに記録され、デバッグ情報を提供します。エラーは呼び出し元に伝播し、適切なレベルでの適切なエラー処理を可能にします。監視システムはこれらのエラーについてアラートを発することができ、アプリケーションは無効な状態で続行するのではなく、迅速に失敗します。

まとめ

空のcatchブロックは、本番コードでは決して許容されません。捕捉された例外は最低限ログに記録する必要があり、ほとんどは呼び出し元に伝播させるか、特定の回復アクションをトリガーする必要があります。エラーを本当に無視する必要がある場合は、そのビジネス上の正当な理由をコメントで説明し、文書化してください。デフォルトでは、エラーは常に明示的に処理し、サイレントに破棄すべきではありません。

よくある質問

ご質問がありますか?

特定の(エラー)を本当に無視する必要がある場合はどうなりますか?

エラーを無視しても安全である理由を説明するコメントを付けて、明示的に文書化します。エラーをデバッグレベルでログに記録し、詳細ログには表示されるがアラートはトリガーされないようにします。エラーを無視することが無効な状態につながる可能性があるかどうかを検討します。キャッシュミスやネットワークタイムアウトのような「予期される」エラーであっても、ロギングは運用チームがシステム動作パターンを理解するのに役立ちます。

catchブロックでは常にエラーをログに記録すべきですか?

何が失敗したかを確認せずに問題をデバッグすることはできないため、ロギングは通常良いアイデアです。エラーをすぐに再スローして他の場所で処理する場合や、致命的な障害でアプリケーションがクラッシュして再起動すべき場合など、ログなしで問題を追跡できるケースもあります。しかし、適切なロギングは常に役立ちます。

ロギングとエラーの再スローの違いは何ですか?

ロギングは、デバッグと監視のために発生した事象を記録します。エラーを再スローすることで、呼び出し元にエラーが伝播され、呼び出し元がどのように対応するかを決定できます。両方を行ってください。エラー発生箇所でコンテキストとともにエラーをログに記録し、その後(より具体的なエラー型でラップして)再スローすることで、呼び出し元が回復処理を行えるようにします。同じエラーを複数のレベルでログに記録しないでください。ノイズが発生します。

finallyブロックで発生するエラーをどのように処理しますか?

finallyブロックでエラーがスローされることは稀であるべきです。エラーが発生しやすい操作(リソースのクローズなど)を実行する必要がある場合は、それらを独自のtry-catchで囲んでください。エラーはログに記録しますが、元のエラーを隠蔽しないようにしてください。一部の言語では、メインのエラーとfinallyブロックのエラーの両方を処理するための構文が提供されています。これらのメカニズムを使用して、両方のエラーコンテキストを保持するようにしてください。

すべてのエラーをログに記録することによるパフォーマンスへの影響についてはどうですか?

ロギングは、ログなしで本番環境の問題をデバッグするコストと比較して安価です。現代のロギングフレームワークは高度に最適化されています。ロギングがパフォーマンスに影響を与えるほど多くのエラーが発生している場合は、エラーを隠すのではなく修正してください。高いエラー率は深刻な問題を示しており、空のcatchブロックはそれを悪化させるだけです。

catchブロックは常にエラーをスローすべきですか、それともエラー値を返すこともできますか?

言語とアーキテクチャに依存します。Promiseを使用するJavaScriptでは、catchからのスローは次のエラーハンドラーに伝播します。catchからエラーオブジェクトを返すと、そのエラーでPromiseが解決されますが、これは通常誤りです。ご使用の言語のエラー処理セマンティクスに精通してください。一般的に、意味のある回復ができない限り、エラーを伝播させてください。

try-catchがない非同期操作でのエラーをどのように処理しますか?

Promiseの.catch()ハンドラー、イベントエミッターのエラーイベントリスナー、またはコールバックベースのAPIにおけるエラーコールバックを使用してください。リジェクションハンドラーやエラーコールバックを無視しないでください。未処理のPromiseリジェクションはプロセスレベルで監視し、重大な障害として扱うべきです。最新のNode.jsは未処理のリジェクションで終了できます。これはサイレントフェイルよりも優れています。

今すぐ、安全な環境へ。

コード、クラウド、ランタイムを1つの中央システムでセキュアに。
脆弱性を迅速に発見し、自動的に修正。

クレジットカードは不要です | スキャン結果は32秒で表示されます。