2019-11-20

JavaScriptのErrorを少しでも他の言語の例外classのように扱えるようにする

まとめ

エラーを定義する際には ES2015 以降の class 構文で以下のようにする。

class CustomError extends Error {
  get name () {
    return 'CustomError'
  }
}

Error

  • JavaScriptMDN</a>

こうしておくと以下の点で便利。

  • name プロパティ ( ここでは実際には getter だけど、外から見れば同じ ) が console などでの表示名として採用されるので見やすい
  • Error の種類の判別を name プロパティの文字列で行うことができる

動作は以下の runtime で確認済み。

  • Node.js 12
  • Safari 13
  • Firefox 70

※ class field は想定していません。これは TC 39 の stage 3 であり、Node.js については 10 では opt-in で利用できる、8 以前では未対応。Babel などのトランスパイラを通せば利用可能ではあるが、pure Node.js ランタイムの利用を考えると、上の getter で定義するのが素直な形。(12以降を前提にできるならそれでもよい。)

cf. tc39/proposal-class-fields: Orthogonally-informed combination of public and private fields proposals

Rubyの例外処理のベストプラクティス

Ruby の場合、例外は以下のように定義するのがベストプラクティスとされている。

module <Namespace>
  class KlassError < StandardError; end
  class KlassSpecificError < KlassError; end

  class Klass
    ..
    raise KlassSpecificError.new
    ..
  end
end
  • まず StandardError を継承して特定のクラスのベースとなる Error クラスを作る
    • StandardError 以外は基本的に runtime 寄りの error となり low level すぎるので避ける
  • ある名前空間内の Error はすべて上で作成したカスタムエラーを継承して Error クラスを作る

こうしておくと最終的にベースとなるカスタムエラークラスを rescue することで特定の名前空間内で発生するエラーに関してはすべて関連するコード内で責任を持って対応することが可能となる。逆に StandardError など、より抽象度の低いエラーを拾いまくって意図しない動作になってしまうことも避けられる。

この class を利用する側では

begin
rescue KlassSpecificError => e
 ..
end

のようにエラーの class を指定して拾うことができる。

これは require した class が global に影響する、module で namespace を構築し衝突を回避できる、この二点によって実現されている。

require / import時代のJavaScriptのclass名はグローバルではない

Ruby では例外の class 名を決めるだけであらかたやることは終わっているのだが、JavaScript では事情が異なる。正確に CommonJS 以降かどうかは勉強不足で知らないのだけど、少なくとも Node.js では

  • exports したもの以外は外部のコードからは直接参照できない
  • exports したものでも require する側で名前の付け替えは自由に行える

ようになっている。ES2015 以降の import / export も同様で、Error に関しては以下のような挙動になる。

dependency.js ( この中で例外が発生する )

class CustomError extends Error {}

class DependencyClass {
  ..
  throw new CustomError()
  ..
}

module.exports = DependencyKlass

dependent.js ( 読み込んだ DependencyClass のコードの中で発生した例外を catch する )

const DependencyKlass = require('./dependency')

class Dependent {
  ..
  try {
  } catch (e) {
    // ここで CustomError という class 名が分からない
  }
  ..
}

上のコードは CustomError を require していないので catch したエラーの class が CustomError であることは分からない。だから上の Ruby のように構文レベルで catch するエラーを指定することはできない。

JavaScrptでErrorクラスを特定する方法

上の問題に対処する方法としては

  1. Error オブジェクトの中の何らかの情報をもとに普通に if で判別する
  2. Error クラスも require する

がある。

しかし、ちょっとやってみれば分かるが解決策として 2 のすべての例外を exports / require するという方法はさすがに非現実的である。そこで 1 になるわけだが、

name プロパティを定義時の class の名前と同じにしておく

のが最も分かりやすくてリーズナブルなので確実と言えると思う。結論としては冒頭に挙げた以下のコードがいちばんよさそう。

class CustomError extends Error {
  get name () {
    return 'CustomError'
  }
}

実際にcatchする方法、テストする方法

catch した例外の name プロパティを見て処理を分ける。

try {
  ..
} catch (e) {
  if ( e.name === 'CustomError' ) {
    ..
  } else {
    throw e
  }
}

テストの際はこれで ok.

assert.throws(
  () => {
    throw new CustomError()
  },
  {
    name: 'CustomError'
  }
)

catch 済みの object が渡ってくるので name でテストする。

Promiseと組み合わせる

Promise の中で Error の発生を記述する方法は throw も reject も同じである。 

func () {
  return new Promise((resolve, reject) => {
    throw new CustomError()
  }
}

func () {
  return new Promise((resolve, reject) => {
    reject(new CustomError())
  }
}

も意味は同じになり、

func()
.catch((e) => {
  e.name === 'CustomError' // <- Error object
})

Promise.catche で Error オブジェクトとして取得できる。catch を書かないと

UnhandledPromiseRejectionWarning: CustomError

のように Node.js では Warning になる。

async/awaitと組み合わせる

async func () {
  throw new Error()
}

これを単に

func()

と呼ぶとやはり先ほどと同じように

UnhandledPromiseRejectionWarning: CustomError

と怒られて Promise が返ってきていることが分かる。これを以下のように実行してみてもやはり同じ。

;(async () => {
  await func()
})()

ただし、以下のようにすると例外として拾うことができる。(Node.js 10で確認。)

;(async () => {
  try {
    await func()
  } catch (e) {
    ..
  }
})()

async/await を使うと一度 Promise にはなるが、同期的なプログラムの時と同じように例外を使って制御することができるようになる。

About

例によって個人のなんちゃらです