自分でエラーを投げる
このレッスンで分かること
- throw は Error オブジェクトを投げる文で、文字列を投げてはいけないこと
- Error を継承した自前のクラスを作ると、失敗の種類ごとに情報を持たせられること
- instanceof で種類を見分け、想定した失敗と想定外の失敗を別々に扱えること
自分でエラーを投げる とは
処理を続けられない条件を見つけた時点で throw して呼び出し元に知らせ、失敗の種類を自前の Error クラスで表す書き方です。
戻り値で失敗を伝えると、無視できてしまう
入力の検証を書くとき、最初に思いつくのは false を返す形である。
function checkTitle(title) {
if (title === "") {
return false;
}
return true;
}
checkTitle(work.title);
saveWork(work);このコードは動く。ただし checkTitle の戻り値を見ずに次の行へ進めてしまう。呼ぶ側が 1 行 if を書き忘れるだけで、検証は素通りになる。しかも false からは「何がだめだったのか」が分からない。項目が複数あると、どの項目でつまずいたのかを別の戻り値で運ぶことになり、戻り値の形がどんどん膨らむ。
throw は逆で、無視できない。投げた時点でその先の行は実行されないので、呼ぶ側が受け止めなければそこで止まる。
失敗を戻り値で返すか throw するかの線引きは「呼ぶ側がふつうに続けてよいか」で決める。作品が 0 件なのは失敗ではないので戻り値でよい。タイトルが空の作品を保存しようとしたのは続けてはいけないので throw にする。
throw には Error を投げる
throw はどんな値でも投げられるが、投げてよいのは Error とその仲間だけである。
throw "タイトルが空です";
throw new Error("タイトルが空です");上の書き方だと、受け取った側の error.name も error.message も undefined になる。error.stack も無いので、どこで起きたかも分からない。捕まえる側が毎回「文字列かもしれない」と身構えることになる。下の書き方に統一する。
Error には最初から仲間がいる。用意されているもののうち、自分で投げる機会があるのは次のあたりである。
| クラス | 使いどころ |
|---|---|
| Error | どれにも当てはまらないとき |
| TypeError | 渡された値の型や形が違うとき |
| RangeError | 値が許される範囲の外にあるとき |
| SyntaxError | 解析できない文字列だったとき。ふつうは処理系が投げる |
Error を継承して自前の種類を作る
第5章でクラスと extends を書いた。エラーも同じ書き方で増やせる。
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}見どころは 3 つある。super(message) を先に呼ぶこと、this.name を自分で書き換えること、そして必要な情報を好きなプロパティとして足せることである。
super(message) を呼ばないと error.message が空になる。this.name を書かないと name は継承元のまま "Error" なので、名前で見分けられない。field のような自前のプロパティは、フォームのどの入力欄を赤くすればよいかを呼び出し元に伝えるために使う。
種類を増やしすぎない。画面の出し分けが変わらないなら分ける必要はない。目安として、catch 側で分岐を書きたくなったときに初めて 1 つ増やす。
instanceof で見分ける
捕まえた側は instanceof で種類を見る。error.name === "ValidationError" でも判定できるが、instanceof なら継承関係も見てくれる。
try {
assertWork(work);
} catch (error) {
if (error instanceof ValidationError) {
showFieldError(error.field, error.message);
} else {
showNotice("想定していないエラーです");
}
}分岐の順番に注意する。ValidationError は Error でもあるので、error instanceof Error を先に書くと自前のクラスがそこで吸われてしまう。細かいほうから先に並べる。
想定外を握りつぶさない
上の else が大事である。assertWork の中で単純な書き間違いがあって TypeError が飛んだとき、それを「入力が不正です」と表示してしまうと、利用者は何度入力し直しても直らない画面と向き合うことになる。想定した失敗と想定外の失敗は、必ず別の文言にする。
投げる側と受ける側を関数で分ける
throw を使うと、検証の関数がとても短くなる。条件に合わないものを見つけたらその場で投げるので、else を書く必要がない。入門で覚えた早期 return と同じ形が、戻り値ではなく例外で並ぶ。
function assertWork(work) {
if (!work || typeof work !== "object") {
throw new TypeError("作品オブジェクトではありません");
}
if (typeof work.title !== "string" || work.title.trim() === "") {
throw new ValidationError("title", "タイトルを入力してください");
}
if (typeof work.year !== "number") {
throw new ValidationError("year", "公開年は数値で入力してください");
}
}この関数は成功したときに何も返さない。返す値が無いのは失敗ではなく、「最後まで来た」ことが合格の合図だからである。名前を assert で始めているのは、戻り値を見る関数ではないと呼ぶ側に伝えるためである。
受ける側は 1 か所にまとめる。
function reviewWork(work) {
try {
assertWork(work);
return { ok: true, field: null, message: "" };
} catch (error) {
if (error instanceof ValidationError) {
return { ok: false, field: error.field, message: error.message };
}
return { ok: false, field: null, message: "想定していないエラーです" };
}
}検証の条件が増えても、書き足すのは assertWork の中の if だけである。reviewWork は触らなくてよい。第7章で正規表現を使った検証を足すときも、同じ場所に並べることになる。
1 回の検証で不備を全部集めたいこともある。throw は最初の 1 件で止まるので、その用途には向かない。全部集めたいときは配列にためて返し、続けてはいけない失敗だけを throw にする。
よくある間違い
this.name を書き忘れる
class ValidationError extends Error {
constructor(message) {
super(message);
}
}
const error = new ValidationError("タイトルが空です");
console.log(error.name);これは "ValidationError" ではなく "Error" と出る。name は継承元の値がそのまま残るので、自分で入れる。
super を呼ばずにプロパティを触る
class ValidationError extends Error {
constructor(field, message) {
this.field = field;
super(message);
}
}これは実行した時点で ReferenceError になる。継承したクラスでは super() を呼ぶまで this を触れない。super を必ず先頭に置く。
検証の途中で throw せず、最後まで進める
function assertWork(work) {
if (work.title === "") {
console.log("タイトルが空です");
}
return work.title.trim();
}console.log は処理を止めないので、その下の行がそのまま動く。止めたいなら throw を書く。
要件
- ValidationError というクラスを Error から継承して作り、field と message を持たせる。name は "ValidationError" にする
- reviewWork(work) を作り、戻り値は { ok, field, message } の 3 つのキーを持つオブジェクトにする
- work がオブジェクトでない場合は ok を false、field を null、message を "想定していないエラーです" にする
- title が空文字列なら field は "title"、message は "タイトルを入力してください" にする
- year が数値でなければ "公開年は数値で入力してください"、2000 未満なら "公開年が古すぎます" にする。どちらも field は "year"
- 不備が無ければ ok を true、field を null、message を空文字列にする
入出力例
reviewWork({"id":1,"title":"プロフィールサイト","year":2026}) → {"field":null,"message":"","ok":true}
reviewWork({"id":2,"title":"","year":2026}) → {"field":"title","message":"タイトルを入力してください","ok":false}
reviewWork({"id":3,"title":"自己紹介カード","year":"2026"}) → {"field":"year","message":"公開年は数値で入力してください","ok":false}
reviewWork({"id":4,"title":"学習ログ","year":1999}) → {"field":"year","message":"公開年が古すぎます","ok":false}
reviewWork(null) → {"field":null,"message":"想定していないエラーです","ok":false}