例外をキャッチして再スローするためのベストプラクティスは何ですか?


156

キャッチした例外は直接再スローする必要がありますか、それとも新しい例外をラップする必要がありますか?

つまり、これを行う必要があります:

try {
  $connect = new CONNECT($db, $user, $password, $driver, $host);
} catch (Exception $e) {
  throw $e;
}

またはこれ:

try {
  $connect = new CONNECT($db, $user, $password, $driver, $host);
} catch (Exception $e) {
  throw new Exception("Exception Message", 1, $e);
}

あなたの答えが直接スローすることである場合は、例外チェーンの使用を提案してください。例外チェーンを使用する実際のシナリオを理解できません。

回答:


287

意味のあることをするつもりでない限り、例外をキャッチするべきではありません。

「意味のあるもの」は次のいずれかです。

例外の処理

最も明白な意味のあるアクションは、たとえばエラーメッセージを表示して操作を中止することにより、例外を処理することです。

try {
    $connect = new CONNECT($db, $user, $password, $driver, $host);
}
catch (Exception $e) {
    echo "Error while connecting to database!";
    die;
}

ロギングまたは部分的なクリーンアップ

特定のコンテキスト内で例外を適切に処理する方法がわからない場合があります。「全体像」についての情報が不足している可能性がありますが、障害が発生した時点のできるだけ近くにログを記録する必要があります。この場合、キャッチ、ログ、および再スローする必要があります。

try {
    $connect = new CONNECT($db, $user, $password, $driver, $host);
}
catch (Exception $e) {
    logException($e); // does something
    throw $e;
}

関連するシナリオは、失敗した操作のクリーンアップを実行するのに適切な場所にいるが、最上位レベルでの失敗の処理方法を決定しない場合です。以前のPHPバージョンでは、これは次のように実装されていました

$connect = new CONNECT($db, $user, $password, $driver, $host);
try {
    $connect->insertSomeRecord();
}
catch (Exception $e) {
    $connect->disconnect(); // we don't want to keep the connection open anymore
    throw $e; // but we also don't know how to respond to the failure
}

PHP 5.5でfinallyキーワードが導入されたため、クリーンアップシナリオでこれに取り組む別の方法があります。クリーンアップコードが何が起こっても(つまり、エラーと成功の両方で)実行する必要がある場合、スローされた例外を透過的に許可しながら、これを実行できるようになりました。

$connect = new CONNECT($db, $user, $password, $driver, $host);
try {
    $connect->insertSomeRecord();
}
finally {
    $connect->disconnect(); // no matter what
}

エラーの抽象化(例外の連鎖あり)

3番目のケースは、考えられる多くの障害をより大きな傘の下で論理的にグループ化する場合です。論理グループの例:

class ComponentInitException extends Exception {
    // public constructors etc as in Exception
}

class Component {
    public function __construct() {
        try {
            $connect = new CONNECT($db, $user, $password, $driver, $host);
        }
        catch (Exception $e) {
            throw new ComponentInitException($e->getMessage(), $e->getCode(), $e);
        }
    }
}

この場合、Componentデータベース接続を使用して実装されていることをユーザーに知られたくない(おそらく、オプションを開いたままにし、将来的にファイルベースのストレージを使用したい)。したがって、の仕様でComponentは、「初期化に失敗した場合はComponentInitExceptionスローされます」と記述されています。これにより、のコンシューマはComponent、予期されるタイプの例外をキャッチできると同時に、デバッグコードがすべての(実装に依存する)詳細にアクセスできるようになります

より豊富なコンテキストの提供(例外チェーンを使用)

最後に、例外のコンテキストを増やしたい場合があります。この場合、エラーが発生したときに何をしようとしていたかについてより多くの情報を保持する別の例外に例外をラップすることは理にかなっています。例えば:

class FileOperation {
    public static function copyFiles() {
        try {
            $copier = new FileCopier(); // the constructor may throw

            // this may throw if the files do no not exist
            $copier->ensureSourceFilesExist();

            // this may throw if the directory cannot be created
            $copier->createTargetDirectory();

            // this may throw if copying a file fails
            $copier->performCopy();
        }
        catch (Exception $e) {
            throw new Exception("Could not perform copy operation.", 0, $e);
        }
    }
}

このケースは上記に似ています(そしておそらくこの例は思いつく最善のものではないでしょう)が、より多くのコンテキストを提供するポイントを示しています。例外がスローされた場合、ファイルのコピーが失敗したことを示しています。しかし、なぜ失敗したのですか?この情報は、ラップされた例外で提供されます(例がはるかに複雑な場合は、複数のレベルが存在する可能性があります)。

これを行うことの価値はUserProfile、ユーザープロファイルがファイルに保存され、トランザクションセマンティクスをサポートするためにオブジェクトを作成するとファイルがコピーされるシナリオを考える場合に示されます。コミットするまでプロファイルのコピー。

この場合、あなたがした場合

try {
    $profile = UserProfile::getInstance();
}

その結果、「ターゲットディレクトリを作成できませんでした」という例外エラーが発生したため、混乱する権利があります。この「コア」例外をコンテキストを提供する他の例外のレイヤーにラップすると、エラーの処理がはるかに簡単になります(「プロファイルのコピーの作成に失敗しました」->「ファイルのコピー操作に失敗しました」->「ターゲットディレクトリを作成できませんでした」)。


私は最後の2つの理由のみに同意します:1 /例外の処理:このレベルではそれを行うべきではありません、2 /ロギングまたはクリーンアップ:最終的に使用し、データレイヤーの上に例外を記録します
レミブルガレル

1
@remi:PHPがfinally構成要素をサポートしていないことを除いて(少なくともまだ)...だから、これはアウトです。つまり、このような汚いものに頼らなければなりません...
ircmaxell

@remibourgarel:1:それは単なる例でした。もちろん、あなたはこのレベルでそれをするべきではありませんが、答えはそうであるように十分に長いです。2:@ircmaxellが言うように、finallyPHPにはありません。
Jon

3
最後に、PHP 5.5がついに実装されました。
OCDev 2013年

12
ここでリストから見逃しているのには理由があります。例外をキャッチして検査するまで、例外を処理できるかどうかを判断できない場合があります。たとえば、エラーコードを使用する(その数は数十億ある)下位レベルのAPIのラッパーには、エラーのインスタンスをスローする単一の例外クラスがあり、そのerror_codeプロパティをチェックして基になるエラーを取得できる場合があります。コード。これらのエラーのいくつかを意味のある方法でしか処理できない場合は、おそらくキャッチして検査し、エラーを処理できない場合は再スローします。
Mark Amery 14

37

まあ、それはすべて抽象化を維持することです。したがって、例外を連鎖させて直接スローすることをお勧めします。なぜかというと、漏れやすい抽象化の概念を説明しましょう

モデルを構築しているとしましょう。モデルは、アプリケーションの残りの部分からすべてのデータの永続性と検証を抽象化することになっています。では、データベースエラーが発生するとどうなるでしょうか。を再スローするDatabaseQueryExceptionと、抽象化がリークします。理由を理解するには、抽象化について少し考えてみてください。モデルがデータをどのように格納するは関係ありません。同様に、モデルの基礎となるシステムで何が問題になっているのかを正確に気にする必要はありません。

したがって、DatabaseQueryExceptionを再スローすることにより、抽象化をリークし、呼び出し元のコードがモデルの下で何が行われているかのセマンティクスを理解する必要があります。代わりに、ジェネリックを作成しModelStorageException、キャッチされたDatabaseQueryException内部をラップします。こうすることで、呼び出し側のコードは意味的にエラーを処理しようとすることができますが、その抽象化レイヤーからのエラーを公開するだけなので、モデルの基礎となるテクノロジーは問題になりません。さらに良いことに、例外をラップしたので、それが完全にバブルしてログに記録する必要がある場合は、スローされたルート例外まで追跡(チェーンをたどる)できるため、必要なデバッグ情報をすべて入手できます。

後処理を行う必要がない限り、同じ例外をキャッチして再スローしないでください。しかし、ブロックのようなもの} catch (Exception $e) { throw $e; }は無意味です。ただし、例外を再ラップして、抽象化を大幅に向上させることができます。


2
すばらしい答えです。スタックオーバーフローの周りのかなりの数の人々(回答などに基づいて)は、それらを間違って使用しているようです。
James

8

私見、例外をキャッチして再スローするだけでは役に立たない。この場合は、それをキャッチせず、以前に呼び出されたメソッドがそれを処理できるようにします(別名、コールスタックで「上位」のメソッド)

再スローする場合、キャッチした例外をスローする新しい例外にチェーンすることは、キャッチした例外に含まれる情報が保持されるため、間違いなく良い方法です。ただし、それを再スローすることは、何らかのコンテキスト、値、ロギング、リソースの解放など、キャッチされた例外に情報追加したり何か処理したりする場合にのみ役立ちます。

いくつかの情報を追加する方法を拡張することであるExceptionような例外有するようにクラスをNullParameterExceptionDatabaseExceptionもっとオーバーなど、これは開発者に対しのみ、彼が扱うことができるいくつかの例外をキャッチすることができます。たとえば、データベースに再接続するなど、唯一の原因を特定して、のDatabaseException原因を解決しようとすることがExceptionできます。


2
それは役に立たないわけではありません。例外をスローする関数で何かを言い、それを再度スローして、上位のキャッチに他のことをさせる必要がある場合があります。私が取り組んでいるプロジェクトの1つで、アクションメソッドの例外をキャッチし、ユーザーにわかりやすい通知を表示してから再度スローすることで、コードのさらに外側のtry catchブロックがエラーをログに記録して、ログ。
MitMaro 2011

1
だから私が言ったように、あなたは例外にいくつかの情報を追加します(通知を表示し、それを記録します)。OPの例のように再スローするだけではありません。
クレメントエレマン

2
まあ、リソースを閉じる必要があるが、追加する追加情報がない場合は、再スローすることができます。私はそれが世界で最もきれいなものではないことに同意しますが、それは恐ろしい
ircmaxell '05

2
@ircmaxell同意、編集して、それを再スローする以外に何もしない
Clement Herreman

1
重要なことは、例外を再スローすることによって、例外が最初にスローされた場所のファイルや行情報を失うことです。したがって、質問の2番目の例のように、通常は新しいものを投げて古いものを渡す方が良いです。それ以外の場合は、catchブロックをポイントするだけで、実際の問題が何であるかを推測できます。
DanMan 2014年

2

PHP 5.3の例外のベストプラクティスを確認する必要があります。

PHPの例外処理は、まったく新しい機能ではありません。次のリンクでは、PHP 5.3の2つの新機能が例外に基づいています。1つ目はネストされた例外で、2つ目はSPL拡張機能(現在はPHPランタイムのコア拡張機能)によって提供される新しい例外タイプのセットです。これらの新機能はどちらも、ベストプラクティスの本に登場し、詳細に検討する価値があります。

http://ralphschindler.com/2010/09/15/exception-best-practices-in-php-5-3


1

通常、このように考えます。

クラスは、一致しない多くのタイプの例外をスローする可能性があります。そのため、そのクラスまたはクラスのタイプの例外クラスを作成し、それをスローします。

したがって、クラスを使用するコードは、1つのタイプの例外をキャッチするだけで済みます。


1
こんにちは、私はこのアプローチについてもっと読むことができるいくつかの詳細またはリンクを提供することができます。
Rahul Prasad
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.