Suppressing Duplicate Async Error Reports
When an Axn async job fails, your error monitoring integration may report the exception twice: once via Axn's on_exception path, and once natively from the background job framework's own error integration.
For example, with Honeybadger + Sidekiq: Honeybadger's Sidekiq plugin independently catches the re-raised exception on every execution, producing a separate raw fault in Honeybadger regardless of your async_exception_reporting setting. If you've configured :first_and_exhausted, you'll still see a raw Sidekiq RuntimeError fault with one notice per retry — which makes the reporting look broken.
General Approach
The fix belongs in your error reporter, not in Axn. Suppress framework-native reports for Axn actions (since Axn is already handling reporting via on_exception) while leaving non-Axn jobs unaffected.
The two signals you need:
- Is this job/notice Axn-owned? Ask
Axn::Async.owns?(signal). It accepts a resolved Class, a String class name (including the ActiveJob adapter's"::ActiveJobProxy"suffix), or a raw Sidekiq job Hash (string or symbol keys), and folds in every detail of Axn's async wiring — the generic Sidekiq worker, the proxy naming convention, and thedisplay_classwire format. Pass it whatever your error reporter's plugin hands you; blank/unrecognized input returnsfalsewithout raising. - Was this notice sent by Axn or by the framework natively? Tag Axn-authored notices in your
on_exceptionhandler so they can be distinguished from the native ones.
Why not just check klass.include?(Axn::Core)?
Because Axn actions are no longer Sidekiq::Jobs. The enqueued class is a generic worker (Axn::Async::Adapters::Sidekiq::Worker — either a per-action AxnSidekiqWorker subclass or the global DefaultWorker) that constantizes and runs your action by name; the real action name only survives as a string in display_class or the first job arg. A klass.include?(Axn::Core) / klass < Axn check against the worker class therefore returns false, silently letting duplicate reports through. Axn::Async.owns? handles all of this, so downstream filters never need to track Axn's internal class hierarchy or wire format.
Add a known key when calling your error reporter from on_exception:
Axn.configure do |c|
c.on_exception = proc do |e, action:, context:|
# Tag this notice as Axn-authored so we can identify it in before_notify filters
Honeybadger.notify(e, context: context.merge(axn: true))
end
endHoneybadger + Sidekiq Example
Honeybadger's before_notify hook lets you inspect and halt notices before they're sent:
# config/initializers/honeybadger.rb
Honeybadger.configure do |config|
config.before_notify do |notice|
# Axn-authored notices (tagged via on_exception) always pass through
next if notice.context[:axn] || notice.context["axn"]
# Halt native Sidekiq/ActiveJob notices for Axn actions —
# Axn's on_exception is handling reporting for these. `owns?` recognizes each
# job-class signal Honeybadger's plugins record, whatever its shape:
# - notice.component (Sidekiq/ActiveJob plugin, a class-name String)
# - notice.parameters[:job] (the raw Sidekiq job Hash: display_class / wrapped / class)
params = notice.parameters
job_hash = params[:job] || params["job"] || params
signals = [notice.component, job_hash]
next notice.halt! if signals.any? { |signal| Axn::Async.owns?(signal) }
end
endWhat this does:
- Axn-authored notices (tagged
axn: true) pass through unchanged - Native Sidekiq/ActiveJob notices for Axn actions are halted
- Notices for non-Axn workers are unaffected
- Multiple
before_notifyhooks compose — this doesn't interfere with other app-specific filters
Notes
- The exact implementation varies by error reporter and adapter. The pattern is the same: detect Axn ownership at the filter layer and suppress native reports, passing through Axn-authored ones.
- This fix is typically applied in your application or framework layer rather than in Axn itself, since it depends on which error reporter you're using.