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の一貫性も保たれる。
Invitation from Wantedly, Inc.
If this story triggered your interest, have a chat with the team?
Wantedly, Inc.'s job postings
25 Likes
25 Likes

Weekly ranking

Show other rankings
Like Masaki Hara's Story
Let Masaki Hara's company know you're interested in their content