エラーはオブジェクト、しかも「型」を持っている
JavaScript でエラーが発生したとき、返ってくるのは単なる文字列ではなく、れっきとしたオブジェクトです。このオブジェクトには 型(コンストラクタ)があり、name・message・stack といった標準プロパティを備えています。
err.name は "TypeError" のような短いラベル、err.message は人間が読むための説明文です。そして err instanceof TypeError を使えば、具体的にどのクラスのエラーなのかを判定できます。エラーの種類を把握することは重要で、それがタイポなのか、値がおかしいのか、それともそもそも構文として読めなかったのかを教えてくれます。
JavaScript の組み込みエラーは全部で7種類あります。そのうち3つは頻繁に出会うもの、残り4つはたまに顔を出す程度のものです。
SyntaxError: コードをパースできなかった
SyntaxError は、JavaScript がそもそもコードを読み取れなかったことを意味します。厳密には実行時エラーですらなく、エンジンは1行も実行する前、パース段階で失敗しています。そのため、同じファイル内で SyntaxError を try/catch することはできません。ファイル全体が丸ごと弾かれてしまうからです。
function greet(name {
return "hi, " + name;
}
// SyntaxError: Unexpected token '{'
カッコの閉じ忘れ、余計なカンマ、関数の外に書かれた return など、構文として成立しないコードはすべてこのエラーになります。対処法はただひとつ、ソースを直すことです。SyntaxError を catch できる数少ないケースは、JSON.parse のように実行時にパースする場面です。
JSON.parse は実行時に文字列を受け取るので、そこで起きる構文エラーは catch で拾えます。一方、自分で書いたソースファイル側の構文エラーは拾えません。
ReferenceError: その名前、どこにも無いよ
ReferenceError は、今いるスコープから見えるどこにも宣言されていない変数を参照したときに発生します。
このエラーの9割はタイポです(total を totl と書いてしまう、みたいなやつ)。残りの1割はスコープの問題で、別の関数やモジュールで宣言された変数を使おうとしているパターンですね。
もう一つ、少しわかりにくい原因があります。いわゆる 一時的デッドゾーン(Temporal Dead Zone, TDZ) です。let や const で宣言した変数はブロックの先頭から存在してはいるものの、実際に宣言している行より前ではアクセスできません。
console.log(x) が実行される時点で x というバインディング自体は存在しているものの、まだ初期化されていない状態です。これが参照エラーの原因です。対処法は、アクセスする行を宣言の後ろに移動することです。
TypeError: 値の型が想定と違う
TypeError は、値自体は存在しているものの、その操作が期待している種類の値ではないときに発生します。関数じゃないものを呼び出した、null や undefined のプロパティを読もうとした、const に再代入した—これらはすべて TypeError です。
「Cannot read properties of null (reading 'name')」は、JavaScriptで遭遇するエラーメッセージの中でもおそらく最も定番のひとつです。対処法としては、値が必ず存在するように保証するか、オプショナルチェイニング(user?.name)でアクセスをガードするかのどちらかになります。
TypeError には他にもいろいろなパターンがあります。
数値を関数として呼び出したり、const を再代入したり、存在しないメソッドを呼んだり——いずれも値の型が、やろうとした操作に合っていないときに起きます。
RangeError: 数値が範囲外
RangeError は、数値としては正しいものの、その操作で許される範囲を超えているときに発生します。
代表的な原因は、無限再帰でコールスタックを吹き飛ばしてしまうケースです。
「Maximum call stack size exceeded」が出たときは、だいたい関数が終了条件なしで自分自身を呼び続けているか、2つの関数がお互いを呼び合って無限ループになっているかのどちらかです。
URIError と EvalError: めったに遭遇しないエラー
URIError は、URI を扱う関数(encodeURI や decodeURIComponent など)に不正な形式の文字列を渡したときに発生します。
EvalError はもはや遺物です。最近の JavaScript エンジンは実際にはこのエラーを投げることはなく、後方互換性のためだけにコンストラクタが残っています。手動で生成することはできますが、実戦で出会うことはまずありません。
継承関係
ここまで紹介したエラー型は、すべて基底の Error を継承しています。つまり、どのエラーでも err instanceof Error は true になるので、汎用的な catch で扱うときに便利です。
catch ブロックは何でもキャッチします。誰かが throw "oops" のように書けば、Error オブジェクトではない値まで飛んでくるわけです。だから最後の分岐が大事で、キャッチした値を Error として扱う前に、必ず instanceof で絞り込むようにしてください。
意図的にエラーを投げる
組み込みのエラー型は、自分のコードで問題を検知したときに自由に throw できます。失敗の内容に合った型を選ぶのがポイントです:
型と失敗の内容をきちんと合わせるのは、見た目を整えるためだけの話ではありません。呼び出し側がエラーメッセージを文字列で解析するような不格好なコードを書かずに、catch で狙い撃ちの処理を書けるようになるのが一番の狙いです。
カスタムエラークラスの作成
組み込みのエラー型ではしっくりこない場合は、Error を継承して自作しましょう。
覚えておきたいポイントは2つ。まず super(message) を呼んで、ベースの Error をきちんと初期化すること。そして this.name をセットして、ログに正しいラベルが出るようにすること。field のようなカスタムプロパティを持たせておけば、呼び出し側はエラーメッセージを文字列解析しなくても、特定の失敗パターンにピンポイントで対応できます。
次は: コンソールと開発者ツール
エラーの種類を押さえたら、あとはもう半分。残りの半分は、スタックトレースを読み解き、実行中のプログラムの状態を覗き見るスキルです。ブラウザの開発者ツール(そして Node のデバッガ)を使えば、「例外は出たけど原因がわからない…」という状況も、ほんの数秒の確認作業に変わります。次の章で取り上げていきます。
よくある質問
JavaScriptに組み込まれているエラーの種類は?
JavaScriptには7種類のエラーが用意されています。基底となるError、そしてSyntaxError、ReferenceError、TypeError、RangeError、URIError、EvalErrorです。どれもコンストラクタになっていて、生成されたエラーオブジェクトはname、message、stackの各プロパティを持ちます。実際に遭遇することが多いのはSyntaxError、ReferenceError、TypeErrorの3つです。
SyntaxErrorとTypeErrorの違いは?
SyntaxErrorは「そもそもJavaScriptとして成立していない」コードで、エンジンがパースすらできない状態です。一方TypeErrorはパースは通ったものの、実行時に不正な操作をしたときに出ます。関数じゃないものを呼び出したり、nullのプロパティを読もうとしたりするケースですね。構文エラーはスクリプト全体が止まりますが、型エラーは該当の行を実行したときに初めて発生します。
ReferenceErrorはどんなときに出る?
宣言されていない名前を使ったとき、またはletやconstで宣言した変数を「一時的デッドゾーン(TDZ)」で触ったときに発生します。原因で一番多いのはタイプミス。たとえばconsoel.log(x)とやるとReferenceError: consoel is not definedが出ます。まずはスペルとスコープを確認しましょう。
独自のエラー型を作れますか?
作れます。組み込みのErrorクラスを継承すればOKです。例:class ValidationError extends Error { }。コンストラクタの中でthis.nameを設定しておくと、ログやcatchブランチで判別しやすくなります。失敗の種類ごとに処理を変えたい場面では、カスタムエラークラスがかなり便利です。