このガイドは、Ruby on Railsアプリケーションで発生するエラーの管理方法を解説します。
本ガイドの内容:
ErrorReporterでエラーをキャプチャして通知する方法RailsのErrorReporterは、アプリケーションで発生したエラーを収集して、好みのサービスや場所に通知する標準的な方法を提供します(例: Sentryなどの監視サービスに通知する)。
この機能の目的は、以下のような定型的なエラー処理コードを置き換えることです。
begin do_something rescue SomethingIsBroken => error MyErrorReportingService.notify(error) end
上の定形コードを、以下のようなインターフェイスで統一できます。
Rails.error.handle(SomethingIsBroken) do do_something end
Railsはすべての実行(HTTPリクエスト、ジョブ、rails runnerの起動など)をErrorReporterにラップするので、アプリで発生した未処理のエラーは、そのサブスクライバを介してエラーレポートサービスに自動的に通知されます。
HTTPリクエストの場合、ActionDispatch::ExceptionWrapper.rescue_responsesに存在するエラーは、サーバーエラー(500)を引き起こさないため通知されません。これらのバグは、一般的に対処不要です。
これにより、サードパーティのエラー通知ライブラリは、Rackミドルウェアを挿入したり、未処理の例外をキャプチャするモンキーパッチを適用したりする必要がなくなります。また、Active Supportを使うライブラリがこの機能を利用して、従来ログに出力されなかった警告を、コードに手を加えずに通知できるようになります。
このエラーレポーターの利用は必須ではありません。エラーをキャプチャする他の手法はすべて引き続き利用できます。
エラーレポーターを利用するにはサブスクライバ(subscriber)が必要です。サブスクライバは、reportメソッドを持つ任意のオブジェクトのことです。アプリケーションでエラーが発生したり、手動で通知されたりすると、Railsのエラーレポーターはエラーオブジェクトといくつかのオプションを使ってこのメソッドを呼び出します。
SentryやHoneybadgerなどのように、自動的にサブスクライバを登録してくれるエラー通知ライブラリもあります。
また、以下のようにカスタムサブスクライバを作成することも可能です。
# config/initializers/error_subscriber.rb class ErrorSubscriber def report(error, handled:, severity:, context:, source: nil) MyErrorReportingService.report_error(error, context: context, handled: handled, level: severity) end end
Subscriberクラスを定義したら、Rails.error.subscribeメソッドを呼び出して登録します。
Rails.error.subscribe(ErrorSubscriber.new)
サブスクライバはいくつでも登録できます。Railsはサブスクライバを登録順に呼び出します。
Rails.error.unsubscribeを呼び出すことで、サブスクライバを登録解除することも可能です。これは、依存関係で追加されたサブスクライバを置換・削除する場合に便利です。subscribeとunsubscribeには、サブスクライバを渡すことも、サブスクライバのクラスを渡すことも可能です。
subscriber = ErrorSubscriber.new Rails.error.unsubscribe(subscriber) # or Rails.error.unsubscribe(ErrorSubscriber)
Railsのエラーレポーターは、どの環境でも常に登録されたサブスクライバーを呼び出します。しかし多くのエラー通知サービスは、デフォルトではproduction環境でのみエラーを通知します。必要に応じて、複数の環境で設定を行ってテストする必要があります。
Railsのエラーレポーターには、エラー通知用の4つのメソッドがあります。
Rails.error.handleRails.error.recordRails.error.reportRails.error.unexpectedRails.error.handleは、ブロック内で発生したエラーを通知してから、そのエラーを握りつぶします。ブロックの外の残りのコードは通常通り続行されます。
result = Rails.error.handle do 1 + "1" # TypeErrorが発生 end result # => nil 1 + 1 # ここは実行される
ブロック内でエラーが発生しなかった場合、Rails.error.handleはブロックの結果を返し、エラーが発生した場合はnilを返します。
以下のようにfallbackを指定することで、この振る舞いをオーバーライドできます。
user = Rails.error.handle(fallback: -> { User.anonymous }) do User.find(params[:id]) end
Rails.error.recordはすべての登録済みレポーターにエラーを通知し、その後エラーを再度raiseします。残りのコードは実行されません。
Rails.error.record do 1 + "1" # TypeErrorが発生 end 1 + 1 # ここは実行されない
ブロック内でエラーが発生しなかった場合、Rails.error.recordはそのブロックの結果を返します。
Rails.error.reportを呼び出して手動でエラーを通知することも可能です。
begin # code rescue StandardError => e Rails.error.report(e) end
渡したオプションは、すべてエラーサブスクライバに渡されます。
Rails.error.unexpectedを呼び出すことで、想定外のエラーを報告できます。
production環境で呼び出された場合、このメソッドはエラーが報告された後にnilを返し、コードの実行を中断せずに続行します。
development環境やtest環境で呼び出された場合、エラーは新しいエラークラスにラップされ(スタックの上位層でrescueされないようにするため)、開発者にデバッグ情報が表示されます。例:
def edit if published? Rails.error.unexpected("[BUG] Attempting to edit a published article, that shouldn't be possible") false end # ... end
このメソッドは、production環境で発生する可能性があるが、通常の利用結果では発生することが想定されていないエラーを適切に処理することを目的としています。
3つのレポートAPI(#handle、#record、#report)はすべて以下のオプションをサポートしています。これらのオプションは、すべての登録済みサブスクライバに渡されます。
handled: エラーが処理されたかどうかを示すBoolean。
デフォルトはtrueです(ただし#recordのデフォルトはfalseです)。
severity: エラーの重大性を表すSymbol。
期待される値は:error、:warning、:infoのいずれか。
#handleでは:warningに設定されます。
#recordでは:errorに設定されます。
context: リクエストやユーザーの詳細など、エラーに関する詳細なコンテキストを提供するHash。
source: エラーの発生源に関するString。
デフォルトのソースは"application"です。
内部ライブラリから通知されたエラーは他のソースを設定する可能性があります(例: Redisキャッシュライブラリは"redis_cache_store.active_support"を設定する可能性があります)。
サブスクライバは、このソースを利用することで興味のないエラーを無視できます。
Rails.error.handle(context: { user_id: user.id }, severity: :info) do # ... end
コンテキストは、contextオプションで設定することも、以下のように#set_context APIで設定することもできます。
Rails.error.set_context(section: "checkout", user_id: @user.id)
この方法で設定されたコンテキストは、contextオプションとマージされます。
Rails.error.set_context(a: 1) Rails.error.handle(context: { b: 2 }) { raise } # 通知されるコンテキスト: {:a=>1, :b=>2} Rails.error.handle(context: { b: 3 }) { raise } # 通知されるコンテキスト: {:a=>1, :b=>3}
Rails.error.handleやRails.error.recordでは、以下のように特定のクラスのエラーだけを通知できます。
Rails.error.handle(IOError) do 1 + "1" # TypeErrorが発生 end 1 + 1 # TypeErrorsはIOErrorsではないので、ここは「実行されない」
上のTypeErrorはRailsのエラー通知レポーターにキャプチャされません。通知されるのはIOErrorおよびその子孫インスタンスだけです。その他のエラーは通常どおりraiseします。
Rails.error.disableを呼び出すことで、ブロック内でサブスクライバにエラーが通知されないようにできます。subscribeやunsubscribeの場合と同様に、サブスクライバ自身を渡すことも、サブスクライバのクラスを渡すことも可能です。
Rails.error.disable(ErrorSubscriber) do 1 + "1" # TypeErrorはErrorSubscriber経由で報告されなくなる end
これは、サードパーティのエラー通知サービスでエラーを別の方法で処理したい場合や、技術スタックの上位で処理したい場合にも有用です。
エラー通知ライブラリは、以下のようにRailtieでライブラリのサブスクライバを登録できます。
module MySdk class Railtie < ::Rails::Railtie initializer "my_sdk.error_subscribe" do Rails.error.subscribe(MyErrorSubscriber.new) end end end
エラーサブスクライバを登録すると、Rackミドルウェアのような他のエラー機構がある場合、エラーが繰り返し通知される可能性があります。他のエラー機構を削除するか、レポーターの機能を調整して、通知済みの例外を通知しないようにする必要があります。
Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。
原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨
本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。
Railsガイド運営チーム (@RailsGuidesJP)
Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。