少しでも例外を安全に扱うために - RubyとJavaScript編 -

今回は最近例外処理でやらかしたので、それをどう防ぐかをまとめてみた。

例外にまつわる悩み

例外についての説明は割愛。

例外って難しい。できるけどやらない方がよいこと、こういう風に設計した方がよいことなどがいくつもある。逆に言うと

よほど気をつけていないとやってはいけないことを簡単に踏み抜いてしまう

しかし、「気をつける」である限り、やはり踏み抜いてしまう。あぁマーフィーの法則。

てなわけで個人的にはあまり積極的にアプリケーションのレイヤー(特にWebアプリ)で例外を使わない(ライブラリでは普通に使う)ようにしてるんだけど、これは JavaScript で多いんだけど、ライブラリが通常の制御フローかのようにカジュアルに例外を扱っているケースがあり、結果どうしてもアプリのレイヤーに例外処理が漏れ出てきてしまったりする。

対処方法 - 自覚的に意図している例外以外を拾わない

例外処理で気をつけるべきことはいくつかあるが、究極的には

自分で意図した例外以外を捕捉しない

に行き着くのではないか。そもそもエラー情報の伝播や制御フローとして例外を使ってはいけない、などいろいろあるが、まずはこれ。この程度であれば、静的解析でなんとかなりそうだ。

Ruby

Rubocop で縛れる。

Style/RescueStandardError :: RuboCop Docs

Style 設定になってしまっているし、StandardError が対象なので分かりにくい名前になるが、要は

rescue する Error を省略してはいけない

というルールを設定できる。具体的には

  Style/RescueStandardError:
  EnforcedStyle: explicit

という設定になる。StandardError を rescue する際に explicit にしなさい、というルール。簡単に言うと以下のような形になる。

# Bad
begin
  ..
rescue => e # Errorを特定していない
  ..
end

# Good
begin
  ..
rescue StandardError => e
  ..
end

Ruby の rescue は対象のエラーを省略すると StandardError を指定したことになるので、明示しようとしまいと挙動は同じなのだが、これを明示せよとすることで、今から自分がどの Error を rescue しようとしているのかを自覚させることができる。

本当に StandardError なのか、より限定的な Error にできないかは人間がチェックすることになるが、雑に拾おうとしているかどうかはこれで分かる。Rubocop の default は explicit なので導入するだけで機能する。

ただし、Standardrb 経由で Rubocop を使っている場合は default で implicit になってしまう。その場合は以下のように上書きする。

require: standard

inherit_gem:
  standard: config/base.yml

Style/RescueStandardError:
  EnforcedStyle: explicit

JavaScript/TypeScript

JavaScript の場合、困ったことに catch する Error を限定するという記法がない。

これは欠陥だろうと思ったのだが、そもそも JavaScript では任意の class が global にいつでもアクセス可能というわけではなく、Error の class を catch に与えようとしても ReferenceError となってしまう可能性が高い。ということはこの catch の文法はこれが限界なのかもしれない。

そこで catch の中で throw し直すことを強制するルールを設ける。

MrLoh/eslint-plugin-no-catch-all: prevent catch blocks that catch all errors without throwing unexpected errors further

え? 例外を投げ直すことを必須にするの? と疑問に思うかもしれないが、以下を見てほしい。

// Bad
try {
  ..
} catch (e) {
  ..
}

// Good
try {
  ..
} catch (e) {
  if (e.name === 'HTTPError') {
  } else {
    throw e // 意図したエラー以外は throw する
  }
}

上記の Ruby や他の例外を扱える言語では catch / rescue 部分の文法に Error を限定する記法が存在する場合が多いが、JavaScript の場合はそういう記法がないため、

catch したあとに意図したエラーじゃないと分かったら throw し直す

ことで擬似的に同じ効果を果たそうというアプローチになる。

これで例えば try の中にうっかりいろんな処理を書いて意図しない Error が catch の中に含まれていても、それは throw し直されているので握りつぶされない、という寸法だ。

なるほど、これはよさそう。

More