---
description: Ruby Expert — Error Handling
alwaysApply: false
---

# Ruby Expert — Error Handling

Fail fast, fail loud, make failures debuggable. Silent rescues turn outages into all-nighters.

## When to Raise

- **Raising is for exceptional conditions:** broken invariants, unrecoverable state, programming errors
- **Expected cases are not exceptions.** Use return values, `nil`, or Result objects
- **Library boundaries:** re-raise with context or wrap in your own error type

```ruby
# Good: Expected "not found" — return nil
def find_user(id) = User.find_by(id: id)

# Good: Required record missing — raise
def fetch_user!(id) = User.find(id)  # Raises RecordNotFound
```

## Rescue Specificity

- **Never** bare `rescue` or `rescue Exception` without immediate re-raise
- **Rescue specific classes:** `rescue ActiveRecord::RecordNotFound`, `rescue Net::OpenTimeout`
- **At boundaries:** rescue, log with context, re-raise or report to error tracking

```ruby
# Good: Specific rescue, then re-raise
def sync_order(id)
  order = Order.find(id)
  external_system.sync(order)
rescue ExternalAPI::RateLimited => e
  Rails.logger.warn("Rate limited for order #{id}: #{e.message}")
  raise
end
```

## Custom Error Classes

- Namespace under your app/gem: `MyApp::ValidationError`
- Inherit from `StandardError` (not `Exception`). Add attributes if useful

```ruby
module MyApp
  class ValidationError < StandardError
    attr_reader :details
    def initialize(message, details = {})
      super(message)
      @details = details
    end
  end
end
```

## Key Rules

- Log before re-raise when adding context; use structured logging
- Never log sensitive data (passwords, tokens, PII)
- `RecordNotFound` → let Rails turn it into 404
- Background jobs: configure retries/dead queues; don't silently drop failures

## Definition of Done

- [ ] No bare `rescue` or `rescue Exception` without immediate re-raise
- [ ] Custom errors namespaced and inherit from `StandardError`
- [ ] Failures at boundaries logged with context and reported
