AppSignal
Rails Error Reporting: Rails.error and ActiveSupport::ErrorReporter

Rails.error is Rails’ built-in error reporter, an ActiveSupport::ErrorReporter instance. Rails.error.handle swallows an exception and reports it; Rails.error.record reports and re-raises; Rails.error.report submits an error directly. Every report goes to each registered subscriber — an object with a report method — which is how error trackers like AppSignal receive Rails 8.1 exceptions.
This guide is the Rails.error reference: what each reporting method does, the exact severity and handled values subscribers receive, how context flows into reports, and how to write — or install — a subscriber. Every behavior described here is verified on Rails 8.1 (activesupport 8.1.3.1) and Ruby 3.4.
What Is the Rails Error Reporter?
Rails 7.0 introduced a framework-level answer to a question every production app faces: when code rescues an exception, where does the report go? The answer used to be a vendor API call at every rescue site. The error reporter decouples the two sides — application code reports errors through one interface, and whatever service should receive them subscribes once.
Rails.error returns the shared reporter instance:
Rails.error.class
# => ActiveSupport::ErrorReporterThe reporting surface is small. Four methods submit errors: handle, record, report, and unexpected (the last arrived in Rails 7.2). The rest manage the pipeline: subscribe and unsubscribe register destinations, set_context and add_middleware enrich reports, disable silences a subscriber for the duration of a block, and debug_mode switches unexpected between reporting and raising.
One thing the reporter deliberately is not: a destination. Rails ships no built-in subscriber, so a report with nobody subscribed goes nowhere. That design is what keeps the API universal — your code stays vendor-neutral, and the error tracker plugs in underneath.
Swallowing Errors with Rails.error.handle
Swallowing an error — rescuing it, reporting it, and moving on without an error page — used to mean a begin block and a manual call into your error tracker’s API:
begin
raise "Oh CRUD!"
rescue => error
Appsignal.report_error(error) do |transaction|
transaction.set_action("StroopwaffleController#request_waffles")
transaction.set_namespace("web")
end
endRails.error.handle collapses that to three lines and keeps tracker APIs out of your application code:
Rails.error.handle do
raise "CRUD!"
end
# => nilIf the block raises, the reporter passes the exception to every subscriber with handled: true and severity: :warning, then swallows it: execution continues, and handle returns nil. If nothing raises, handle returns the block’s value. When nil is a poor stand-in at the call site, pass fallback: — a callable whose result is returned when the block fails:
recommendations = Rails.error.handle(fallback: -> { [] }) do
raise "recommendation service down"
end
recommendations
# => []By default, handle catches StandardError and its descendants — the same boundary as a bare rescue. Our guide to rescuing exceptions in Ruby covers where that line sits and when to rescue yourself instead of handing off. To narrow the net, pass the class you expect, and anything else propagates unreported:
Rails.error.handle(ArgumentError) do
raise TypeError, "not matched"
end
# TypeError is re-raised — nothing is reportedReporting with Rails.error.record and Rails.error.report
Rails.error.record is handle’s louder sibling: it reports the exception with handled: false and severity: :error, then re-raises it. Use it when the operation should still fail — the exception keeps propagating to whatever rescues, retries, or crashes with it. (For the mechanics on the raising side, see ensure, retry, and reraise exceptions in Ruby.)
Rails.error.record do
raise "boom"
end
# reported with handled: false, severity: :error — then the RuntimeError re-raisesWhen there is no block to wrap because you already hold a rescued exception, Rails.error.report submits it directly, with the report’s metadata under your control:
begin
1 / 0
rescue ZeroDivisionError => error
Rails.error.report(error, severity: :info, context: { job: "DailyDigest" }, source: "my_gem")
endseverity accepts :error, :warning, or :info (anything else raises ArgumentError) and defaults to :warning with handled: true. source defaults to "application"; libraries report under their own source string so subscribers can filter them out. report requires a real exception object — handed anything else, it raises ArgumentError (for a plain string, unexpected is the method that accepts one).
The four reporting methods in one place:
| Method | Takes | Reports with | Afterwards | Returns |
|---|---|---|---|---|
handle | a block | handled: true, severity: :warning | swallows the error | block value; on error, nil or fallback: value |
record | a block | handled: false, severity: :error | re-raises the error | block value |
report | an exception | your keywords (default handled: true, severity: :warning) | execution continues | nil |
unexpected | an exception or string | handled: true, severity: :warning | continues in production; raises in debug mode | nil |
Flagging the Impossible with Rails.error.unexpected
Rails.error.unexpected reports a state that should never occur — the broken invariant rather than the failed operation. In a method on an order model, that looks like this:
def discounted_total(order)
if order.discount_code.nil?
Rails.error.unexpected("Order #{order.id} is discounted but has no code")
return order.total
end
order.total - order.discount
endWhat happens next depends on debug_mode. In production, unexpected reports quietly — handled: true, severity: :warning — and returns nil, so execution continues down your recovery path. A string argument is wrapped in a RuntimeError before reporting. In development and test, the same call raises, so a broken assumption fails loudly while you are there to see it. Rails flips the switch through config.consider_all_requests_local: when that setting is true — the development and test default — the boot process turns debug_mode on.
ActiveSupport::ErrorReporter::UnexpectedError
The error raised in debug mode is not the one you passed in. Save this as unexpected_error.rb:
require "rails"
Rails.error.debug_mode = true
Rails.error.unexpected("boom")Running it prints:
unexpected_error.rb:4:in '<main>': RuntimeError: boom (ActiveSupport::ErrorReporter::UnexpectedError)
unexpected_error.rb:4:in '<main>': boom (RuntimeError)
unexpected wraps your error in an ActiveSupport::ErrorReporter::UnexpectedError whose message carries the original class and message, with the original attached as its cause (the second line of the output). The wrapper subclasses Exception directly, not StandardError — deliberately. A bare rescue => e will not catch it, and neither will Rails.error.handle, so no generic rescue between the assertion and your eyes can silence it. Only in production-style operation, with debug_mode off, does unexpected soften into a quiet report.
Adding Context to Error Reports
Every reporting method takes a context: hash that travels to subscribers with the report:
Rails.error.handle(context: { tag1: "value1", tag2: "value2" }) do
raise "Tagged CRUD!"
endFor context that applies to everything reported during the current request or job, set_context merges keys into the execution context once (Rails resets that context between requests and jobs):
Rails.error.set_context(section: "checkout", user_id: 42)Subscribers decide what context means. AppSignal’s subscriber reads one reserved key, appsignal:, for its own settings — a custom namespace and action for the incident:
Rails.error.handle(context: { appsignal: { namespace: "custom_namespace", action: "CustomizedActionName#index" } }) do
raise "Contextual CRUD!"
end(The Appsignal.set_namespace and Appsignal.set_action helpers do the same for the whole surrounding transaction rather than a single report.) Every other context key becomes a tag on the error, and tags are filterable in your list of incidents:

Cross-cutting context — an app version, a deployment region — belongs in a context middleware, which runs on every report before subscribers see it and returns the (possibly modified) context hash:
Rails.error.add_middleware(
->(error, context:, handled:, severity:, source:) { context.merge(app_version: "2.4.1") }
)Writing an Error Subscriber
A subscriber is any object with a report method that accepts the error plus four keyword arguments. This one writes reports to the Rails log:
class ErrorLogSubscriber
def report(error, handled:, severity:, context:, source: nil)
Rails.logger.warn(
"[#{severity}] #{error.class}: #{error.message} " \
"(handled: #{handled}, source: #{source}, context: #{context.inspect})"
)
end
end
Rails.error.subscribe(ErrorLogSubscriber.new)Subscribe in an initializer, and every handle, record, report, and unexpected call in the app reaches the subscriber — parts of Rails, such as Active Job and the rails runner command, report through the same interface. Rails.error.unsubscribe(ErrorLogSubscriber) removes it again, and Rails.error.disable(ErrorLogSubscriber) { ... } skips it for the duration of a block (in a test that raises on purpose, say). A subscriber that itself raises does not take the report down with it: in production, where boot points Rails.error.logger at Rails.logger, the failure is logged as fatal instead.
The pipeline isn’t limited to controller code, either — background jobs, service objects, and API frameworks can all report through it. (For Grape APIs, which bring their own rescue semantics, see handling exceptions in Grape for Ruby.)
Monitoring Rails Exceptions with AppSignal
You rarely need to write that subscriber yourself. AppSignal for Ruby registers one automatically in Rails apps: every Rails.error.handle, record, and report call lands in AppSignal’s error tracking with the action and namespace detected from the surrounding request or job, and your context keys become filterable tags. One gem turns Rails’ reporting API into full error monitoring.
The integration shipped in AppSignal for Ruby 3.4.1 and is on by default in instrumented Rails apps (the enable_rails_error_reporter config option turns it off). Each report’s severity and source arrive as tags alongside your own, so incidents stay filterable by how — and from where — they were reported.
Wrapping Up
Rails.error gives every rescue site the same three verbs — swallow with handle, escalate with record, submit with report — plus unexpected for the states that should never happen. Subscribers turn those reports into somewhere useful to look, whether that is a log line you wrote yourself or an AppSignal dashboard. Wire one up, and the begin-block era of error reporting is behind you.
Happy coding!
P.S. If you’d like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!
Frequently asked questions
- What is the difference between Rails.error.handle and Rails.error.record?
- handle swallows the exception after reporting it, returning nil or a fallback value, with warning severity. record reports the exception with error severity and then re-raises it, so your code still fails.
- How do I subscribe to the Rails error reporter?
- Call Rails.error.subscribe with any object that responds to report, receiving the error plus handled, severity, context, and source keyword arguments. Every handle, record, report, and unexpected call then reaches your subscriber.
- What does Rails.error.unexpected do?
- It reports a should-never-happen condition. In production it reports the error with warning severity and execution continues; in development and test it raises an UnexpectedError so you notice the broken assumption immediately.
- Does Rails report errors anywhere by default?
- No. The error reporter is only an API: without a subscriber, reports go nowhere. Error tracking services subscribe to it to deliver reports to a dashboard.
Published , Updated
Wondering what you can do next?
- Try out AppSignal with a 30-day free trial.
- Reach out to our support team with any feedback or questions.
- Share this article on social media

Connor James
Developer Marketing Manager at AppSignal. Podcast addict who loves cannoli so much that he's considering changing his name to Connoli. He thinks there's a `u` in color. You might find him on the mic, on the stage, or lying on the sofa when he's off duty.
All articles by Connor James
Tom de Bruijn
Tom is a developer at AppSignal, organizer, and writer from Amsterdam, The Netherlands.
All articles by Tom de BruijnBecome our next author!
AppSignal monitors your apps
AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!


