順番に読み進めながら学べます

例外を発生させる — 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
}
throw に渡したときだけ処理が止まる
if の中にnew Error だけError オブジェクトが作られる何も起きずに次の行へ進む1200 * 0 で0 が返るif の中でthrow new ErrorError オブジェクトが作られるその場で例外が起きるcatch の errorに値が届く
どちらの書き方でも Error オブジェクトは作られます。throw に渡したときだけ、その場で処理が止まります。

検証の if は、計算より前の関数の先頭に置きます。throw は return と同じくその場で関数を抜けるので、後ろの return unitPrice * quantity; は実行されず、呼び出し側が 0 を受け取ることもありません。

アンケートを保存する前に、満足度を確かめます。scores は宣言済みです。

① 整数でなければ「満足度は整数で指定してください」という例外を投げてください。

② 1〜5 の範囲外なら「満足度は 1〜5 で指定してください」という例外を投げてください。

③ 問題が無ければ「満足度 4 で回答しました」の形で返してください。

④ scores を順に渡し、失敗しても次へ進んで、結果か失敗の文言を表示してください。

(正しく実行できれば解説が表示されます)

JavaScript / TypeScript エディタ

コードを実行してください

入力の誤りを表す種類を作る — 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
}
extends Error で作った種類の位置
Error — message と name を持つ例外の種類
  • new Error("文言") で作った例外も、この枠の中の種類も、instanceof Error は true
組み込みの種類 — JavaScript が最初から用意している
  • SyntaxError / TypeError / RangeError — 前の記事で見た種類
extends Error で作った種類
  • ValidationError — 入力の誤りを表す
  • name は class の中で書いた "ValidationError"
  • instanceof ValidationError が true になるのは、この枠の中だけ
どちらも Error の枠の中にあり、互いの枠に入りません。instanceof ValidationError は自分の枠でだけ true です。

throw には文字列も渡せますが、文字列には name も message も無いので、catch の側で error.message を読むと undefined です。下の表は、throw に渡す値ごとに catch で読める内容をまとめたものです。

throw に渡した値catch で読める nameinstanceof Error の結果
"郵便番号が不正です"(文字列)undefined(文字列には無い)false
new Error("郵便番号が不正です")Errortrue
name を書いた ValidationErrorValidationErrortrue
name を書かなかった ValidationErrorError(引き継いだまま)true

ファイルを受け付けない理由を表示します。QuotaError、files、checkFile は宣言済みです。

① Error を引き継いだ FormatError を定義し、name を設定してください。

② checkFile で、拡張子が pdf でなければ「PDF だけを受け付けます」の FormatError を投げてください。

③ files を順に確かめ、戻り値か「name: message」を表示してください。

JavaScript / TypeScript エディタ

コードを実行してください

作った種類から先に判定する — 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("保存できませんでした");
}
判定の順番で入る枝が変わる
instanceof Errorを先に書くValidationErrorでも true後ろの判定は試されない「保存できませんでした」と出るValidationErrorの判定を先に書くValidationErrorから作ったので trueError の判定は試されない「入力を確認してください」と出る
上の段では 1 つ目の判定が true になり、2 つ目は試されません。Error を引き継いだ種類は、Error より先に判定します。

TypeError などの組み込みの種類も Error を引き継いでいるので、後ろに置いた instanceof Error の枝でまとめて受け止められます。入力を直しても解決しない例外には、こちらの枝で別の案内を出します。

メールアドレスの変更を受け付けます。ValidationError、changeEmail、requests は宣言済みです。

① requests を 1 件ずつ changeEmail に渡し、結果を表示してください。

② ValidationError のときは「メールアドレスを確認してください: 」に続けて文言を表示してください。

③ それ以外の Error のときは「変更できませんでした: 」に続けて種類の名前を表示してください。

JavaScript / TypeScript エディタ

コードを実行してください

元の例外を添えて投げる — 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
}
cause の中に元の例外が残る
error — readPayment が投げた PaymentError
  • error.name は PaymentError
  • error.message は 決済結果を読み込めません
error.cause — JSON.parse が投げた元の例外
  • error.cause.name は SyntaxError
  • error.cause.message は JSON.parse が出した説明文のまま
外側は PaymentError で、cause に元の SyntaxError が入ります。元の例外は error.cause に丸ごと残ります。

cause を書かずに差し替えると error.cause は undefined で、元の種類も説明文も読み出せません。呼び出し側は error.name で決済の失敗として案内を分け、記録には error.cause の内容を残す、という使い分けができます。

受け止められなかった例外は実行を止める

投げた例外は 未捕捉の例外(どの catch にも受け取られなかった例外)になると、実行中のコードはそこで止まり、残りの行は 1 行も実行されません。上のコードで外側の try を外すと、PaymentError: 決済結果を読み込めません のように種類の名前と文言が表示されます。

出勤記録を取り込みます。ImportError と records は宣言済みです。

① importRecords で各時刻の先頭 5 文字を取り出し、「E-104: 09:02」の形で表示してください。

② 取り出せなかったら元の例外を添え、「E-105 の行で取り込みを中止しました」の ImportError を投げてください。

③ importRecords を呼び、失敗したら文言と、元の例外の種類を「元の例外: 」に続けて表示してください。

JavaScript / TypeScript エディタ

コードを実行してください
QUIZ

理解度チェック

まずは1問ずつ答えてみましょう。

Q1class ShippingError extends Error {} から作った例外の name は何ですか?

Q2instanceof Error を先に判定すると、ValidationError の例外はどの枝に入りますか?

Q3cause を添えて投げた例外を catch (error) で受け取りました。元の例外の name を読む書き方は?