Spot the Tax · Card 12 of 20
Rescue only what you can recover from
Why a generic rescue => e hides every bug, including the ones you wrote last week.
The code
What will this cost you in six months?
def sync_user(user)
begin
ExternalApi.update(user.id, user.attributes)
rescue => e
Rails.logger.error "Sync failed for user #{user.id}: #{e.message}"
end
end The problem
A bare rescue => e catches every StandardError. Network timeouts, JSON parse errors, typos in your code, missing methods, renamed parameters — they all hit the same log line and the method silently returns nil. The call site can't tell anything went wrong, the UI happily reports success, and the audit log says "synced". Six weeks later somebody notices that 4% of users haven't been synced since the last deploy, and the only trace is grep-able lines in production logs that nobody reads.
Take a moment. Before revealing, ask yourself: which exceptions can this code actually do something useful about? Everything else should probably propagate.
The solution
Narrow the rescue to the specific exceptions you can actually handle. If you only know how to recover from network timeouts, only rescue those. Anything else should propagate up to your error tracker, where it'll get reported alongside a stack trace that lets someone fix the bug.
- Real bugs reach Sentry / Honeybadger instead of being logged and forgotten
- The recovery code targets specific failures you understand
- The call site can rely on errors being raised when something unexpected happens
def sync_user(user)
ExternalApi.update(user.id, user.attributes)
rescue Net::ReadTimeout, Errno::ECONNREFUSED => e
Rails.logger.error "Sync timed out for #{user.id}: #{e.message}"
ExternalApiSyncJob.perform_later(user.id)
end The principle at play — Targeted error handling
Exceptions exist to interrupt the normal flow when something the code can't deal with happens. Catching one means "I know what this is and I have a plan for it", not "shut up and don't crash". A bare rescue says the second thing while pretending to do the first.
The cost of an over-broad rescue is that it doesn't distinguish between recoverable problems (network glitches, rate limits) and unrecoverable ones (a typo in your method name, a missing migration, a serialization error in your own code). Recoverable problems get retried; unrecoverable ones are bugs that need to be fixed. Lumping them together means the bugs get silenced and the recovery never happens for the cases that actually need it.
The rule is to rescue the smallest set of exceptions you can actually do something useful about. Everything else propagates — to a higher-level handler that knows more, or all the way up to your error tracker, where someone can see the stack trace and fix the underlying cause.