Q1class ShippingError extends Error {} から作った例外の name は何ですか?
例外を発生させる — throw とカスタムエラー
自分で例外を発生させる throw new Error と、Error を引き継いで独自の種類を作るカスタムエラーを扱います。instanceof で判定する順番、元の例外を残す cause まで確かめます。
注文画面から数量 0 が届いても、小計を出す関数はそのまま計算して 0 円の注文ができます。注意を表示するだけでは、呼び出し側は失敗に気づかず保存へ進みます。
この記事では、自分で例外を発生させる throw new Error と、種類を自分で定義する カスタムエラー を扱います。
不正な値で処理を止める — throw new Error
数量に小数やマイナスが渡されたときは、小計の計算へ進ませたくないとします。return null で知らせても、呼び出し側が確かめ忘れると、送料を足す null + 500 が 500 として計算が続きます。
Error オブジェクト(例外として投げるための、name と message を持つ値)は new Error("文言") で作ります。作った値を throw に渡すとその場で例外が起き、try の中で呼んでいれば catch の error にその値が届きます。
function calcSubtotal(unitPrice, quantity) {
// 1 以上の整数でなければ、その場で例外を発生させる
if (!Number.isInteger(quantity) || quantity < 1) {
// new Error だけでは止まらない。throw に渡して例外にする
throw new Error(`数量は 1 以上の整数で指定してください: ${quantity}`);
}
return unitPrice * quantity;
}
try {
console.log(calcSubtotal(1200, 3)); // 3600
console.log(calcSubtotal(1200, 0)); // この行の表示は実行されない
console.log("注文を確定しました"); // この行も実行されない
} catch (error) {
console.log(error.name); // Error
console.log(error.message); // 数量は 1 以上の整数で指定してください: 0
}
検証の if は、計算より前の関数の先頭に置きます。throw は return と同じくその場で関数を抜けるので、後ろの return unitPrice * quantity; は実行されず、呼び出し側が 0 を受け取ることもありません。
入力の誤りを表す種類を作る — extends Error
new Error で投げた例外は、検証の内容が何であっても name が Error です。郵便番号の桁数の誤りも在庫不足も同じ名前で届くため、catch の側では利用者に入力を直してもらう失敗かどうかを見分けられません。
class(new で作るオブジェクトの種類を定義する構文)に extends Error を付けると、Error の動きを引き継いだ種類を作れます。波括弧の中に name = "ValidationError"; と書くと、この種類から作った例外の name がその文字列になります。
// Error を引き継いで、入力の誤りを表す種類を作る
class ValidationError extends Error {
name = "ValidationError"; // new で作った例外の name になる
}
function checkZipCode(zipCode) {
if (zipCode.length !== 7) {
throw new ValidationError(`郵便番号は 7 桁で入力してください: ${zipCode}`);
}
return zipCode;
}
try {
checkZipCode("150-00");
} catch (error) {
console.log(error.name); // ValidationError
console.log(error.message); // 郵便番号は 7 桁で入力してください: 150-00
console.log(error instanceof Error); // true
}
new Error("文言")で作った例外も、この枠の中の種類も、instanceof Errorはtrue
SyntaxError/TypeError/RangeError— 前の記事で見た種類
ValidationError— 入力の誤りを表すnameは class の中で書いた"ValidationError"- instanceof ValidationError が true になるのは、この枠の中だけ
throw には文字列も渡せますが、文字列には name も message も無いので、catch の側で error.message を読むと undefined です。下の表は、throw に渡す値ごとに catch で読める内容をまとめたものです。
| throw に渡した値 | catch で読める name | instanceof Error の結果 |
|---|---|---|
| "郵便番号が不正です"(文字列) | undefined(文字列には無い) | false |
| new Error("郵便番号が不正です") | Error | true |
| name を書いた ValidationError | ValidationError | true |
| name を書かなかった ValidationError | Error(引き継いだまま) | true |
作った種類から先に判定する — instanceof の順番
入力の誤りなら利用者に直してもらえますが、TypeError のような想定外の例外は入力を直しても解決しません。catch の中で 2 つの案内を分けたいのに、判定を書く順番を誤ると、入力の誤りにまで想定外の例外向けの案内が出ます。
if と else if は上から試し、最初に当てはまった枝だけを実行します。ValidationError の例外は instanceof Error でも true なので、先に書いた判定が instanceof Error なら、ValidationError の例外もその枝に入ります。
class ValidationError extends Error {
name = "ValidationError";
}
const error = new ValidationError("郵便番号は 7 桁で入力してください");
// Error を先に判定すると、ValidationError もこの枝に入る
if (error instanceof Error) {
console.log("保存できませんでした"); // 保存できませんでした
} else if (error instanceof ValidationError) {
console.log("入力を確認してください"); // ここには来ない
}
// 引き継いだ種類を先に判定する
if (error instanceof ValidationError) {
console.log("入力を確認してください"); // 入力を確認してください
} else if (error instanceof Error) {
console.log("保存できませんでした");
}
TypeError などの組み込みの種類も Error を引き継いでいるので、後ろに置いた instanceof Error の枝でまとめて受け止められます。入力を直しても解決しない例外には、こちらの枝で別の案内を出します。
元の例外を添えて投げる — cause と未捕捉の例外
決済サービスの応答を読む関数で SyntaxError が起きたとき、そのまま投げ直すと、呼び出し側には決済の処理で失敗したことが種類から伝わりません。かといって新しい種類の例外に差し替えると、今度は元が SyntaxError だったことが失われます。
cause(新しく作る例外に、原因になった元の例外を持たせる指定)は、new Error("文言", { cause: error }) のように 2 番目の引数へ書きます。extends Error で作った種類でも同じで、受け取った側は error.cause から元の例外を読み出せます。
class PaymentError extends Error {
name = "PaymentError";
}
function readPayment(text) {
try {
return JSON.parse(text);
} catch (error) {
// 元の例外を cause に添えて、決済用の種類で投げる
throw new PaymentError("決済結果を読み込めません", { cause: error });
}
}
try {
readPayment('{"orderId":"P-3107","amount":}');
} catch (error) {
console.log(error.name); // PaymentError
console.log(error.message); // 決済結果を読み込めません
console.log(error.cause.name); // SyntaxError
}
error.nameはPaymentErrorerror.messageは決済結果を読み込めません
error.cause.nameはSyntaxErrorerror.cause.messageは JSON.parse が出した説明文のまま
cause を書かずに差し替えると error.cause は undefined で、元の種類も説明文も読み出せません。呼び出し側は error.name で決済の失敗として案内を分け、記録には error.cause の内容を残す、という使い分けができます。
受け止められなかった例外は実行を止める
投げた例外は 未捕捉の例外(どの catch にも受け取られなかった例外)になると、実行中のコードはそこで止まり、残りの行は 1 行も実行されません。上のコードで外側の try を外すと、PaymentError: 決済結果を読み込めません のように種類の名前と文言が表示されます。
理解度チェック
まずは1問ずつ答えてみましょう。
Q2instanceof Error を先に判定すると、ValidationError の例外はどの枝に入りますか?
Q3cause を添えて投げた例外を catch (error) で受け取りました。元の例外の name を読む書き方は?