JavaScriptでは任意の値を例外としてthrowすることができますが、実際にはErrorのインスタンスをthrowするのが慣例です。
エラーの原因をより正確に説明したいときはErrorを継承するのが望ましいですが、単に継承するのではなく以下のように書くのがオススメです。
class MyError extends Error {
static {
this.prototype.name = "MyError";
}
}
その背景について以下で説明します。テーマは以下の3つです。
- nameプロパティ
- captureStackTrace
- causeプロパティ
nameを正しくセットする
Node.jsでエラーを表示させると、クラス名が正しく表示されます。
> throw new (class C extends Error {})()
Uncaught C [Error]
ここで出力されている "C" はクラス自身のnameプロパティに由来しています。
> (class C {}).name
'C'
しかし、エラーにはインスタンスのnameプロパティというのも存在し、そちらには期待しない値が入っています。
> new (class C extends Error {})().name
'Error'
エラーレポートなどを正しく動作させるために、このインスタンスのnameプロパティも正しく設定しておくことが望ましいとされています。
現代のJavaScriptではclass static blockが使えるので、次のように書くのが一番綺麗でしょう。
class MyError extends Error {
static {
this.prototype.name = "MyError";
}
}
ところで、これは以下のように書くこともできますが、この書き方は推奨しません。
class MyError extends Error {
static {
this.prototype.name = this.name;
}
}
それは、コードをminifyしたときに this.name
の内容も一緒に変化してしまうからです。
最初に示したコード例のように名前を文字列リテラルとして明示することで、minifyされずにエラー名を保持することができます。強い難読化の必要がなければ、エラー名はそのまま保持するほうがよいでしょう。
スタックトレース
MDNのガイドを見ると、「スタックトレースを正しく取得するため」としてコンストラクタ内でcaptureStackTraceを呼んでいる例があります。
class MyError extends Error {
constructor(message) {
super(message);
// スタックトレースの取得 (???)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, MyError);
}
}
}
しかし、現代ではこの対応は基本的に不要だと考えられます。captureStackTraceはJavaScript処理系の独自実装であるため、以下ではcaptureStackTraceを提供する代表的な処理系であるV8を前提に説明します。
captureStackTraceの使い方はV8のドキュメントに説明されています。要点をまとめると、これは以下の目的で使用することができます。
- 任意のオブジェクトにスタックトレースを埋め込めるようにするため。
- 埋め込んだスタックトレースを適切にトリミングするため。
これはカスタムエラーをclassによらずに作る場合には確かに有用です。たとえば、以下のような例を考えます。
function MyError() {
}
MyError.prototype = Object.create(Error.prototype);
throw new MyError();
この方法ではstack traceが入らないため、captureStackTraceを明示的に呼ぶ意味が出てきます。
しかし、これはclassを使っている場合には必要ありません。classを使っている場合、
- Errorコンストラクタ内で自動的にスタックトレースが収集されます。
- Errorコンストラクタ内では `new.target` に指定された関数より上のフレームをスキップするように設定されます。これはcaptureStackTraceにクラス名 (MyError) を指定するのと同じ挙動です。
- new.target を見ているため、Babelを使ってES5の構文までトランスパイルしていても正しく動作するはずです。BabelはReflect.constructがあればそれをスーパーコンストラクタ呼び出しに利用するためです。
以上のことからスタックトレースに関しては心配する必要はありません。どのような経緯でMDNにこの記述が残ったのかは不明ですが、2023年現在この設定は不要だと考えられます。
causeとオプション
最新のJavaScriptではErrorはcauseを取ることができるようになっています。これにより、エラーを別のエラーでラップしたときの因果関係を統一的に扱うことができます。
try {
// ...
} catch (e) {
if (e instanceof Error) {
throw new MyError("Failed to ...", { cause: e });
} else {
throw e;
}
}
カスタムエラークラスでも、コンストラクタを再定義しなければこのまま動作します。
もしコンストラクタを再定義するときは、cause引数を意識した定義にするのをおすすめします。以下はlocというカスタムプロパティを持つエラークラスを定義する例です。
class ParseError extends Error {
static {
this.prototype.name = "ParseError";
}
constructor(message = "", options = {}) {
const { loc, ...rest } = options;
// causeがあるときはErrorに渡される
super(message, rest);
this.loc = loc;
}
}
まとめ
- static blockでthis.prototype.nameを初期化するとよい。
- minifyによってクラス名が変わってしまうことがあるため、this.prototype.nameは明示的な文字列リテラルとして初期化するのが望ましい。
- Errorを継承さえしていれば、 captureStackTrace の必要はない。
- コンストラクタをオーバーライドするときは (message, options) 形式にして未使用オプションをそのままスーパーコンストラクタに渡すようにすれば、causeも一緒に渡すことができAPIの一貫性も保たれる。