Rails のエラー処理について知ってる範囲でまとめ

Ruby / Rails のエラー処理をそのままの状態でリリースすると色々と問題があるので、
それに対してどういうことをやったりしているかのまとめ。

```
Ruby: 2.3.1p112
Rails: 4.2.7
```

特定の場所のエラーを拾う
-

何らかのエラーが発生した場合、
そのまま Rails の英語のエラー画面を出してしまうと、
ユーザーに無駄に悪い印象を与えてしまったりするので、
表側に出す用のエラー画面を用意して遷移させておく。

``` ruby
begin
  # 普通の処理
rescue => e
  # エラー時の処理
end
```

のようにすると、普通の処理部分でエラーが発生したら、エラー時の例外処理を実行できる。
エラー時の処理では、ログ出力したり、表側用のエラー画面を出したりする。

``` ruby
  logger.error e 
  logger.error e.backtrace.join("\n") 

  flash[:alert] += '更新に失敗しました'
  render :edit
```

ログ出力が不十分だと、どんなエラーが起こったかを捕捉できなくなり、
障害発生時に苦労したり詰むハメになるので、
ログに十分な情報を出すようにしなければならない。

メソッド中身全部から拾いたい場合は

``` ruby
def method
  begin
    # 普通の処理 
  rescue => e
    # エラー時の処理 
  end
end
```

　　↓

``` ruby
def method
  # 普通の処理
rescue => e
  # エラー時の処理 
end
```

のように省略できる。

``` ruby
rescue => e
```

というのは

``` ruby
rescue StandardError => e
```

の略みたいなものらしく、
通常のプログラムで起こる一般的なエラー群の`StandardError`を拾うようになっているので、
通常はこのままでいい。

あえて拾いたいエラーが他にある場合は

``` ruby
begin
  # 普通の処理
rescue ActiveRecord::RecordInvalid => e # 他のエラーを指定
  # ActiveRecord::RecordInvalid が発火した時の処理
rescue => e
  # 上で拾われなかった StandardError が発火したの時の処理
end
```

のように、`StandardError`の前に指定して拾う。

なお、この`ActiveRecord::RecordInvalid`は`.save!`のバリデーションエラーで発火するので、
他のシステムエラーと区別したい時にこういう風に書いたりする。

アプリケーション全体でエラーを拾う
-

application_controller.rb で

``` ruby
  rescue_from ActiveRecord::RecordNotFound, with: :render_404

  def render_404(e = nil)
    if e
      logger.error e 
      logger.error e.backtrace.join("\n") 
    end

    render template: 'errors/error404', status: 404, layout: 'application', content_type: 'text/html'
  end
```
のように書くと、
`ActionController::RecordNotFound`がどこで起こっても`render_404`メソッドが実行され、
404用のエラー画面を出したり、エラーをログに出力したりするようになる。
要は、エラーを拾いたい場所に、いちいちエラー処理を書かなくても拾えるようになる。

この全体で拾うエラー処理は、場所ごとで拾うエラー処理より優先度が低いので、
場所ごとに書いたエラー処理を上書きしてしまう心配はない（はず）。

エラーログを整理
-

上記のに加えて、application_controller.rb に

``` ruby
  def log_app_error(e, log_level = :error)
    logger.send(log_level,  <<LOG)

#{e.class} (#{e.message}):
 #{Rails.backtrace_cleaner.clean(e.backtrace).join("\n").indent(1)}

LOG
  end
```

のようなエラーログ出力メソッドを書き、
ログを出したいシーンで

``` ruby
    log_app_error e
```

を書くと、

``` ruby
NoMethodError (undefined method `nai_method' for nil:NilClass):
  app/controllers/companies_controller.rb:6:in `index`
```

のような、無駄を省いた感じの backtrace を出せるようになる。


development 環境では表側用のエラー画面に移動させない
-

表側用のエラー画面は、本番環境では出した方がいいが、
ローカルやステージング環境では出す意義が薄く、
普通に Rails のエラー画面を出した方がエラーに気づきやすいので、
development 環境では出さないようにしておくといい。

``` ruby
  rescue_from ActiveRecord::RecordNotFound, with: :render_404 unless Rails.env.development?
```
のように`unless Rails.env.development?`のような条件を付けると、
ローカルでは出ないようになる。

複数の rescue_from を書く
-

``` ruby
  rescue_from StandardError, with: :render_500 unless Rails.env.development?
  rescue_from ActiveRecord::RecordNotFound, with: :render_404 unless Rails.env.development?
```

のように並べて書くと、
`ActiveRecord::RecordNotFound`が起こった場合は 404 画面、
`StandardError`が起こった場合は 500 画面を出すといった出し分けができる。

`rescue_from`は、どういうわけか下のものほど優先度が高いらしいので、
上記の例だと、仮に両方起こった場合、
下にある`render_404`のエラー処理が実行されることになる。

rescue の対象には広すぎるクラスを指定しないようにする
-

Ruby の例外クラスの構造はこんな感じになっている。
（ https://docs.ruby-lang.org/ja/latest/library/_builtin.html の「例外クラス」より  ）

* Exception
    * NoMemoryError
    * ScriptError
        * LoadError
        * NotImplementedError
        * SyntaxError
    * SecurityError
    * SignalException
        * Interrupt
    * StandardError
        * ArgumentError
        * （大量）
        * ZeroDivisionError
    * SystemExit
    * SystemStackError
    * fatal

※ Rails も含めるともっと増え、`ActiveRecord`関連等の大量のクラスが`StandardError`に入る。 

例えば`StandardError`を指定すると、その子の`ZeroDivisionError`等も拾うようになる。

そのため、`Exception`のような、あまり広い範囲のエラーを拾うようにしてしまうと、
`Interrupt`（Ctrl+C 等の割り込み）のように、拾うとまずい系も拾ってしまうので、
拾っても大丈夫なのだけを拾うようにしなければならない。

どの例外クラスを拾うべきか
-

考えてみた結果がこちら。
（もちろんただの一例で、こうするべきという話ではないです）

### 拾いたい
`StandardError`
通常のプログラムで発生する可能性の高い例外クラスを集めたものとのことなので、拾うべき。
gem が追加した例外クラスも、ほとんどがここに入る模様。

### 拾いたくない
`Interrupt, SystemExit`
割り込みを邪魔されないようにしたいため。

### 拾えない
`fatal`
そもそも通常の方法ではアクセス自体できないとのこと。

### 自分には判断できない
`NoMemoryError, ScriptError, SecurityError, SystemStackError`
これらは拾っても問題なさそうな気がしていたが、
調べてみると、拾わない方がいい説も割とあるので、
自分的には拾うべきかが分からなくなってきたというもの。
今回の例では入れないでおく。

（`Exception`は、バージョンアップ等で余計なものが入りうるので指定していない）

rescue 節内から raise で脱出
-

http://qiita.com/ktarow/items/9d8f3217bb148f2e51d2#raise%E3%81%AE%E8%BF%BD%E5%8A%A0
によると、

> raiseが発生した時点で実行中のrescue節が終了し、呼び出し元に戻っています。

とのことで、
`rescue`節内で`raise`を引数なしで実行すると、
`rescue`で拾っているエラーを再発生させ、呼び出し元に戻るらしい。

これを利用すると、
`rescue`で特定のエラーを拾い、それ用の処理（ログ出力等）を追加して、
本来のエラー処理へ戻すことができる。

`Interrupt`とかを止めたくないけど、それらを捕捉しなくても本当に大丈夫なのか心配だから、
一応念のためログ出力だけしておいたという処理がこんな感じ。

``` ruby
  rescue_from NoMemoryError, ScriptError, Interrupt, SecurityError, SignalException, SystemExit, SystemStackError, with: :only_logging unless Rails.env.development?

  def only_logging(e = nil)
    log_app_error e if e
    raise
  end
```

まとめ
-

こうして application_controller.rb にできたエラー処理がこんな感じ。

``` ruby
  rescue_from StandardError, with: :render_500 unless Rails.env.development?
  rescue_from NoMemoryError, ScriptError, Interrupt, SecurityError, SignalException, SystemExit, SystemStackError, with: :only_logging unless Rails.env.development?
  rescue_from ActiveRecord::RecordNotFound, ActionController::RoutingError,with: :render_404 unless Rails.env.development?

  def render_404(e = nil)
    log_app_error e if e
    render template: 'errors/error404', status: 404, layout: 'application', content_type: 'text/html'
  end

  def render_500(e = nil)
    log_app_error e if e
    render template: 'errors/error500', status: 500, layout: 'application', content_type: 'text/html'
  end

  def only_logging(e = nil)
    log_app_error e if e
    raise
  end
```

その他
-

ログに出るだけでは、通知の意味では不十分なので、
サーバー側で起こった深刻な種類のエラーを確実に通知したければ、
特定の種類のエラーのレポートを、システム管理者のメアドに送るようにもできる。
（筆者はその方法をちゃんと調べていないので、ここには書かない）

あまりどうでもいいエラーを送るようにしてしまっていると、
見る側の時間を浪費したり、いずれ重要なのごとスルーされるようになってしまうので、
どうでもいいエラーは送らないようにすることも重要と思われる。