UGA Boxxx

つぶやきの延長のつもりで、知ったこと思ったこと書いてます

【JavaScript】Error を拡張する

JavaScriptで作ったアプリでのエラーをハンドリングしたい

例えば、Node.jsのAPIサーバーでリクエストパラメータの精査エラーをハンドリングするなど

特定のエラーを処理する場合はMDNにあるようにinstanceofで判別することができる

try {
  foo.bar()
} catch (e) {
  if (e instanceof EvalError) {
    console.error(e.name + ': ' + e.message)
  } else if (e instanceof RangeError) {
    console.error(e.name + ': ' + e.message)
  }
  // ... etc
}

developer.mozilla.org

なので、 Errorを継承したカスタムエラークラスを作ればよいかと思ったが、ちょっと知らないことがあったのでメモ

単純に継承する場合は以下のようにする

class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = "ValidationError";
  }
}

function run() {
  throw new ValidationError("エラーメッセージ");
}

try {
  run();
} catch(err) {
  console.log(err);
}

これで、ちゃんとinstanceofで判別できるし、スタックトレースも期待通り表示される

ValidationError: エラーメッセージ
    at run (/Users/uga/playground/error-handing/test.js:9:9)
    at Object.<anonymous> (/Users/uga/playground/error-handing/test.js:13:3)
    at Module._compile (node:internal/modules/cjs/loader:1092:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1121:10)
    at Module.load (node:internal/modules/cjs/loader:972:32)
    at Function.Module._load (node:internal/modules/cjs/loader:813:14)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:76:12)
    at node:internal/main/run_main_module:17:47

こんな要領でValidationErrorよりもうちょっと詳細な、必須エラー(RequiredError)や最大文字数エラー(MaxLengthError)などを作くりたくなって増やしたのだが、

class RequiredError extends Error {
  constructor(props) {
    super(`"${props}"は必須項目です。`);
    this.name = "RequiredError";
    this.props = props;
  }
}
class MaxLengthError extends Error {
  constructor(props) {
    super(`"${props}"は10文字以上にしてください。`);
    this.name = "MaxLengthError";
    this.props = props;
  }
}

this.name = "XXXXError";のところが毎回手書きで、スペルミスに注意したりして手間だと感じた

そんなとき、コンストラクタでthis.name = this.constructor.name;としておくとクラス名がnameにセットされることがわかった

class RequiredError extends Error {
  constructor(props) {
    super(`"${props}"は必須項目です。`);
    this.name = this.constructor.name;
    this.props = props;
  }
}

とすれば良いが、それすらも手間なので、ベースとなるカスタムクラスを作って継承させるとスッキリする

class BaseError extends Error {
  constructor(message) {
    super(message);
    this.name = this.constructor.name;
  }
}

class RequiredError extends BaseError {
  constructor(props) {
    super(`"${props}"は必須項目です。`);
    this.props = props;
  }
}
class MaxLengthError extends BaseError {
  constructor(props) {
    super(`"${props}"は10文字以上にしてください。`);
    this.props = props;
  }
}

次に、これらのカスタムエラー以外のSyntaxErrorReferenceErrorTypeErrorなどを捉えて、共通的なメッセージを表示させたい

例えば、以下のようにrun関数を定義した場合、(1)でTypeErrorが発生するが、スタックトレースには任意のメッセージは入れられない

function run(name) {
  if (!name) {
    throw new RequiredError("name");
  }
  const firstKanjiName = name.first.kanji   // --------(1)
}

try {
  run("uga");
} catch(err) {
  console.log(err);
}

Errorを捕らえるために、run関数でtry-catchを行い、新たなRunErrorをつくることにした

function run(name) {
  try {
    if (!name) {
      throw new RequiredError("name");
    }
    const firstKanjiName = name.first.kanji   // --------(1)
  } catch (error) {
    throw new RunError("run実行中にエラーが発生しました。", error);
  }
}

RunErrorは以下のように実装しておく

class RunError extends BaseError {
  constructor(message, cause) {
    super(message);
    this.cause = cause;
  }
}

これで、(1)でTypeErrorを発生させると以下のような形でスタックトレースに表示された

RunError: run実行中にエラーが発生しました。
    at run (/Users/uga/playground/error-handing/test.js:38:11)
    at Object.<anonymous> (/Users/uga/playground/error-handing/test.js:43:3)
    at Module._compile (node:internal/modules/cjs/loader:1092:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1121:10)
    at Module.load (node:internal/modules/cjs/loader:972:32)
    at Function.Module._load (node:internal/modules/cjs/loader:813:14)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:76:12)
    at node:internal/main/run_main_module:17:47 {
  cause: TypeError: Cannot read property 'kanji' of undefined
      at run (/Users/uga/playground/error-handing/test.js:36:39)
      at Object.<anonymous> (/Users/uga/playground/error-handing/test.js:43:3)
      at Module._compile (node:internal/modules/cjs/loader:1092:14)
      at Object.Module._extensions..js (node:internal/modules/cjs/loader:1121:10)
      at Module.load (node:internal/modules/cjs/loader:972:32)
      at Function.Module._load (node:internal/modules/cjs/loader:813:14)
      at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:76:12)
      at node:internal/main/run_main_module:17:47

これで、原因追跡のための情報を仕込んだりして調査を楽にすることができそう

参考

https://ja.javascript.info/custom-errors

https://v8.dev/docs/stack-trace-api