165
148

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

EngraphiaAdvent Calendar 2016

Day 24

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

Last updated at Posted at 2016-12-23

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

Ruby: 2.3.1p112
Rails: 4.2.7

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

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

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

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

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

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

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

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

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

  ↓

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

のように省略できる。

rescue => e

というのは

rescue StandardError => e

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

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

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

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

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

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

application_controller.rb で

  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 に

  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

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

    log_app_error e

を書くと、

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

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

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

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

  rescue_from ActiveRecord::RecordNotFound, with: :render_404 unless Rails.env.development?

のようにunless Rails.env.development?のような条件を付けると、
ローカルでは出ないようになる。

複数の rescue_from を書く

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

  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 にできたエラー処理がこんな感じ。

  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

その他

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

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

165
148
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
165
148

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?