Ruby & Rails

Exceptions that tell you what to do

Rescuing broadly turns a specific failure into a mystery. The useful distinction is between what you expected and what you didn't.

14 January 2026 2 min read Mohammad Aaquib Jawed

rescue => e catches everything and usually logs a message that will not help anyone. Six months later somebody is trying to work out why a record silently failed to save.

Expected versus unexpected

Expected failures are part of the domain: invalid input, a record that no longer exists, a payment declined, a third party returning 503. Your code should handle these explicitly, because they will happen and there is a correct response.

Unexpected failures are bugs: a nil where there should not be one, a typo'd method, a contract violated. These should be loud, reach your error reporter, and not be caught by a broad rescue that turns them into a shrug.

Most damage comes from treating the second category as the first — a rescue intended for a network timeout also swallowing a NoMethodError, so the bug ships and nobody knows.

Rescue narrowly, and near the problem

Rescue the specific class you anticipated. Rescue as close to the operation as you can, so the handler knows what failed and what to do about it.

A rescue wrapping fifty lines cannot know which of ten operations failed, so its recovery is necessarily generic — which usually means logging and continuing in an unknown state.

Say what happened and what to do

An error message is read by someone under pressure. "Something went wrong" costs them an hour.

Include what was attempted, with which identifiers, and what the caller might do about it. For a user-facing message, that is an action they can take. For a log line, it is enough context to reproduce without guessing.

A good error message is written for the person reading it at 3am, not for the person writing it at 3pm.

Custom classes are cheap

Defining your own error classes takes one line each and lets callers distinguish failures without parsing strings.

A small hierarchy — one base class per area, specific subclasses beneath — means a caller can rescue broadly or narrowly as it chooses, and adding a new failure mode does not break existing handlers.

Retry is a decision, not a reflex

Retrying makes sense when the failure is transient and the operation is safe to repeat. Both halves matter, and the second is usually the one nobody checked.

Retrying a non-idempotent operation is how a customer gets charged twice. Retrying a permanent failure is how a queue fills with work that will never succeed.

All writing Reply by email