何かを開発するとき、タスク内で問題が発生する可能性のある特定の事柄を反映するために、独自のエラー クラスが必要になることがよくあります。ネットワーク操作のエラーの場合はHttpError
、データベース操作の場合はDbError
、検索操作の場合はNotFoundError
などが必要になる場合があります。
私たちのエラーは、 message
、 name
、そしてできればstack
などの基本的なエラー プロパティをサポートする必要があります。ただし、それらは独自の他のプロパティを持つこともあります。たとえば、 HttpError
オブジェクトは、 404
、 403
、または500
のような値を持つstatusCode
プロパティを持つことがあります。
JavaScript では任意の引数とともにthrow
を使用できるため、技術的にはカスタム エラー クラスはError
から継承する必要はありません。しかし、継承すると、 obj instanceof Error
使用してエラー オブジェクトを識別することが可能になります。したがって、それを継承する方が良いでしょう。
アプリケーションが成長するにつれて、私たち自身のエラーは自然に階層を形成します。たとえば、 HttpTimeoutError
HttpError
などから継承する場合があります。
例として、ユーザー データを含む JSON を読み取る関数readUser(json)
を考えてみましょう。
有効なjson
どのように見えるかの例を次に示します。
let json = `{ "名前": "ジョン", "年齢": 30 }`;
内部的にはJSON.parse
を使用します。不正な形式のjson
受信すると、 SyntaxError
がスローされます。しかし、たとえjson
構文的に正しいとしても、それが有効なユーザーであることを意味するわけではありません。必要なデータが欠落している可能性があります。たとえば、ユーザーにとって不可欠なname
とage
プロパティが含まれていない可能性があります。
関数readUser(json)
JSON を読み取るだけでなく、データをチェック (「検証」) します。必須フィールドがない場合、または形式が間違っている場合は、エラーになります。データは構文的に正しいため、これはSyntaxError
ではなく、別の種類のエラーです。これをValidationError
と呼び、そのクラスを作成します。この種のエラーには、問題のあるフィールドに関する情報も含まれている必要があります。
ValidationError
クラスはError
クラスを継承する必要があります。
Error
クラスは組み込みですが、何を拡張しているのかを理解できるように、おおよそのコードを次に示します。
// JavaScript 自体によって定義される組み込み Error クラスの「疑似コード」 クラスエラー{ コンストラクター(メッセージ) { this.message = メッセージ; this.name = "エラー"; // (組み込みエラー クラスごとに異なる名前) this.stack = <呼び出しスタック>; // 標準ではありませんが、ほとんどの環境でサポートされています } }
次に、 ValidationError
継承して実際に動作させてみましょう。
class ValidationError extends Error { コンストラクター(メッセージ) { スーパー(メッセージ); // (1) this.name = "検証エラー"; // (2) } } 関数テスト() { throw new ValidationError("おっと!"); } 試す { テスト(); キャッチ(エラー) { アラート(err.メッセージ); // おっと! アラート(err.name); // 検証エラー アラート(err.スタック); // ネストされた呼び出しのリストとそれぞれの行番号 }
注意してください: 行(1)
では、親コンストラクターを呼び出します。 JavaScript では子コンストラクターでsuper
呼び出す必要があるため、これは必須です。親コンストラクターはmessage
プロパティを設定します。
親コンストラクターはname
プロパティも"Error"
に設定するため、行(2)
で正しい値にリセットします。
これをreadUser(json)
で使用してみましょう。
class ValidationError extends Error { コンストラクター(メッセージ) { スーパー(メッセージ); this.name = "検証エラー"; } } // 使用法 関数 readUser(json) { let user = JSON.parse(json); if (!user.age) { throw new ValidationError("フィールドなし: 年齢"); } if (!user.name) { throw new ValidationError("フィールドがありません: 名前"); } リターンユーザー。 } // try..catch を使用した実際の例 試す { let user = readUser('{ "年齢": 25 }'); } キャッチ (エラー) { if (err 検証エラーのインスタンス) { alert("無効なデータ: " + err.message); // 無効なデータ: フィールドがありません: 名前 } else if (err instanceof SyntaxError) { // (*) alert("JSON 構文エラー: " + err.message); } それ以外 { エラーをスローします。 // 不明なエラー。再スローします (**) } }
上記のコードのtry..catch
ブロックは、 ValidationError
とJSON.parse
からの組み込みSyntaxError
の両方を処理します。
(*)
行で、 instanceof
使用して特定のエラー タイプをチェックする方法を見てください。
次のようにerr.name
を調べることもできます。
// ... // (errinstanceofSyntaxError) の代わりに } else if (err.name == "SyntaxError") { // (*) // ...
将来的にはValidationError
拡張し、 PropertyRequiredError
のようなそのサブタイプを作成する予定であるため、 instanceof
バージョンの方がはるかに優れています。また、 instanceof
チェックは、新しい継承クラスに対しても引き続き機能します。つまり、それは将来性があります。
また、 catch
不明なエラーに遭遇した場合、それを行(**)
に再スローすることも重要です。 catch
ブロックは検証エラーと構文エラーの処理方法のみを知っており、他の種類のエラー (コード内のタイプミスやその他の不明な理由が原因) は失敗するはずです。
ValidationError
クラスは非常に汎用的です。多くのことがうまくいかない可能性があります。プロパティが存在しないか、間違った形式 ( age
の数値ではなく文字列値など) である可能性があります。存在しないプロパティに対して正確に、より具体的なクラスPropertyRequiredError
を作成しましょう。不足しているプロパティに関する追加情報が含まれます。
class ValidationError extends Error { コンストラクター(メッセージ) { スーパー(メッセージ); this.name = "検証エラー"; } } class PropertyRequiredError extends ValidationError { コンストラクター(プロパティ) { super("プロパティがありません: " + プロパティ); this.name = "プロパティ必須エラー"; this.property = プロパティ; } } // 使用法 関数 readUser(json) { let user = JSON.parse(json); if (!user.age) { throw new PropertyRequiredError("age"); } if (!user.name) { throw new PropertyRequiredError("name"); } リターンユーザー。 } // try..catch を使用した実際の例 試す { let user = readUser('{ "年齢": 25 }'); } キャッチ (エラー) { if (err 検証エラーのインスタンス) { alert("無効なデータ: " + err.message); // 無効なデータ: プロパティ: 名前がありません アラート(err.name); // プロパティ必須エラー アラート(err.property); // 名前 else if (err 構文エラーのインスタンス) { alert("JSON 構文エラー: " + err.message); } それ以外 { エラーをスローします。 // 不明なエラーなので再スローしてください } }
新しいクラスPropertyRequiredError
は使いやすいです。プロパティ名new PropertyRequiredError(property)
を渡すだけです。人間が判読できるmessage
はコンストラクターによって生成されます。
PropertyRequiredError
コンストラクターのthis.name
が再び手動で割り当てられることに注意してください。すべてのカスタム エラー クラスにthis.name = <class name>
を割り当てるのは、少し面倒になるかもしれません。 this.name = this.constructor.name
を割り当てる独自の「基本エラー」クラスを作成することで、これを回避できます。そして、そこからすべてのカスタム エラーを継承します。
これをMyError
と呼びましょう。
MyError
とその他のカスタム エラー クラスを簡略化したコードを次に示します。
class MyError extends Error { コンストラクター(メッセージ) { スーパー(メッセージ); this.name = this.constructor.name; } } class ValidationError extends MyError { } class PropertyRequiredError extends ValidationError { コンストラクター(プロパティ) { super("プロパティがありません: " + プロパティ); this.property = プロパティ; } } // 名前は正しいです alert( new PropertyRequiredError("field").name ); // プロパティ必須エラー
コンストラクター内の"this.name = ..."
行が削除されたため、カスタム エラー、特にValidationError
が大幅に短くなりました。
上記のコード内の関数readUser
の目的は、「ユーザー データを読み取る」ことです。プロセス中にさまざまな種類のエラーが発生する可能性があります。現時点ではSyntaxError
とValidationError
がありますが、将来的にreadUser
関数が拡張され、おそらく他の種類のエラーが生成される可能性があります。
readUser
呼び出すコードは、これらのエラーを処理する必要があります。現時点では、 catch
ブロック内で複数のif
を使用しており、クラスをチェックして既知のエラーを処理し、未知のエラーを再スローします。
スキームは次のとおりです。
試す { ... readUser() // 潜在的なエラーの原因 ... } キャッチ (エラー) { if (err 検証エラーのインスタンス) { // 検証エラーを処理します else if (err 構文エラーのインスタンス) { // 構文エラーを処理します } それ以外 { エラーをスローします。 // 不明なエラーなので再スローしてください } }
上記のコードでは 2 種類のエラーが確認できますが、さらに多くのエラーが存在する可能性があります。
readUser
関数が複数の種類のエラーを生成する場合は、自問する必要があります。本当にすべての種類のエラーを毎回 1 つずつチェックする必要があるのでしょうか。
多くの場合、答えは「ノー」です。私たちは「それよりも 1 つ上のレベル」になりたいと考えています。私たちが知りたいのは「データ読み取りエラー」があったかどうかだけです。正確な理由は多くの場合無関係です (エラー メッセージで説明されています)。あるいは、必要な場合に限り、エラーの詳細を取得する方法があればさらに良いのですが。
ここで説明する手法は「例外のラッピング」と呼ばれます。
一般的な「データ読み取り」エラーを表す新しいクラスReadError
を作成します。
関数readUser
その内部で発生するデータ読み取りエラーValidationError
やSyntaxError
など) を捕捉し、代わりにReadError
生成します。
ReadError
オブジェクトは、そのcause
プロパティに元のエラーへの参照を保持します。
そうすれば、 readUser
呼び出すコードは、あらゆる種類のデータ読み取りエラーではなく、 ReadError
をチェックするだけで済みます。エラーの詳細が必要な場合は、そのcause
プロパティを確認できます。
ReadError
を定義し、 readUser
とtry..catch
での使用法を示すコードは次のとおりです。
class ReadError extends Error { コンストラクター(メッセージ、原因) { スーパー(メッセージ); this.cause = 原因; this.name = '読み取りエラー'; } } class ValidationError extends Error { /*...*/ } class PropertyRequiredError extends ValidationError { /* ... */ } 関数 validateUser(user) { if (!user.age) { throw new PropertyRequiredError("age"); } if (!user.name) { throw new PropertyRequiredError("name"); } } 関数 readUser(json) { ユーザーに許可します。 試す { ユーザー = JSON.parse(json); } キャッチ (エラー) { if (err 構文エラーのインスタンス) { throw new ReadError("構文エラー", err); } それ以外 { エラーをスローします。 } } 試す { validateUser(ユーザー); } キャッチ (エラー) { if (err 検証エラーのインスタンス) { throw new ReadError("検証エラー", err); } それ以外 { エラーをスローします。 } } } 試す { readUser('{悪い json}'); } キャッチ (e) { if (e インスタンス of ReadError) { アラート(e); // 元のエラー: SyntaxError: JSON の位置 1 に予期しないトークン b があります alert("元のエラー: " + e.cause); } それ以外 { eを投げます。 } }
上記のコードでは、 readUser
説明どおりに動作します。構文エラーと検証エラーをキャッチし、代わりにReadError
エラーをスローします (不明なエラーは通常どおり再スローされます)。
したがって、外側のコードは、 instanceof ReadError
をチェックするだけです。考えられるすべてのエラーの種類をリストする必要はありません。
このアプローチは、「低レベル」例外を取得し、より抽象的なReadError
に「ラップ」するため、「例外のラッピング」と呼ばれます。オブジェクト指向プログラミングで広く使用されています。
通常は、 Error
および他の組み込みエラー クラスを継承できます。 name
プロパティを処理する必要があるだけで、 super
呼び出すことを忘れないでください。
特定のエラーをチェックするには、 instanceof
使用します。継承でも機能します。しかし、サードパーティのライブラリからエラー オブジェクトが発生し、そのクラスを取得する簡単な方法がない場合があります。その後、 name
プロパティをそのようなチェックに使用できます。
例外のラッピングは広く普及している手法です。つまり、関数が低レベルの例外を処理し、さまざまな低レベルのエラーの代わりに高レベルのエラーを作成します。低レベルの例外は、上記の例のerr.cause
のように、そのオブジェクトのプロパティになることがありますが、これは厳密には必須ではありません。
重要度: 5
組み込みのSyntaxError
クラスを継承するクラスFormatError
を作成します。
message
、 name
、およびstack
プロパティをサポートする必要があります。
使用例:
let err = new FormatError("フォーマットエラー"); アラート( err.メッセージ ); // フォーマットエラー アラート( err.name ); // フォーマットエラー アラート( err.stack ); // スタック alert( err インスタンスの FormatError ); // 真実 アラート( err インスタンスオブ構文エラー ); // true (SyntaxError から継承されるため)
class FormatError extends SyntaxError { コンストラクター(メッセージ) { スーパー(メッセージ); this.name = this.constructor.name; } } let err = new FormatError("フォーマットエラー"); アラート( err.メッセージ ); // フォーマットエラー アラート( err.name ); // フォーマットエラー アラート( err.stack ); // スタック アラート( err 構文エラーのインスタンス ); // 真実