1
/
5

JavaScriptのカスタムエラーはこれでOK

Photo by Syed Ali on Unsplash

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の一貫性も保たれる。
Wantedly, Inc.'s job postings
21 Likes
21 Likes

Weekly ranking

Show other rankings
Invitation from Wantedly, Inc.
If this story triggered your interest, have a chat with the team?