Framework Cookbooks

Ruby on Rails Error Handling Cookbook

Practical patterns for error handling in Rails: rescue_from in controllers, custom error pages, API error responses, ActiveRecord validation errors, and background job handling.

Rails rescue_from in Controllers

rescue_from is a DSL that maps exception classes to handler methods at the controller level. It works via inheritance — declare it in ApplicationController and all controllers inherit it:

class ApplicationController < ActionController::Base
  rescue_from ActiveRecord::RecordNotFound, with: :not_found
  rescue_from ActionController::ParameterMissing, with: :bad_request
  rescue_from Pundit::NotAuthorizedError, with: :forbidden

  private

  def not_found(exception)
    respond_to do |format|
      format.html { render 'errors/404', status: :not_found }
      format.json { render json: error_body('not_found', exception.message), status: :not_found }
    end
  end

  def bad_request(exception)
    render json: error_body('bad_request', exception.message), status: :bad_request
  end

  def forbidden
    render json: error_body('forbidden', 'You are not authorized'), status: :forbidden
  end

  def error_body(code, message)
    { error: { code: code, message: message } }
  end
end

Rails symbol aliases for status codes are human-readable: :not_found (404), :unprocessable_entity (422), :forbidden (403), :created (201).

Custom Error Pages (404, 500)

In production, Rails delegates unhandled errors to config.exceptions_app. Point it at your own router for full control:

# config/application.rb
config.exceptions_app = self.routes

# config/routes.rb
match '/404', to: 'errors#not_found', via: :all
match '/422', to: 'errors#unprocessable', via: :all
match '/500', to: 'errors#internal_server_error', via: :all

# app/controllers/errors_controller.rb
class ErrorsController < ApplicationController
  def not_found
    render 'errors/404', status: :not_found
  end

  def internal_server_error
    render 'errors/500', status: :internal_server_error
  end
end

Important: Set config.consider_all_requests_local = false in production or Rails will render its debug error page instead of your custom one.

API Error Responses with Jbuilder/Serializers

For JSON APIs, use Jbuilder or Active Model Serializers to produce consistent error envelopes:

# With Jbuilder (app/views/errors/_error.json.jbuilder)
json.error do
  json.code @error_code
  json.message @error_message
  json.status response.status
end

# In controller:
def not_found(exception)
  @error_code = 'not_found'
  @error_message = exception.message
  render 'errors/error', status: :not_found
end

With render json:, build the error hash directly for simplicity in API-only applications (ActionController::API):

class Api::BaseController < ActionController::API
  rescue_from ActiveRecord::RecordNotFound do |e|
    render json: {
      type: 'https://protocolcodes.com/http/404',
      title: 'Not Found',
      status: 404,
      detail: e.message,
    }, status: :not_found
  end
end

ActiveRecord Validation Errors

Validation errors are 422 Unprocessable Entity. Serialize model.errors for the response body:

def create
  @user = User.new(user_params)
  if @user.save
    render json: @user, status: :created
  else
    render json: {
      error: 'validation_failed',
      errors: @user.errors.as_json,
    }, status: :unprocessable_entity
  end
end

@user.errors.as_json returns a hash of field → [messages]. For a flat list, use @user.errors.full_messages.

Background Job Error Handling

Active Job wraps errors and retries jobs automatically. Configure retry behavior per job class:

class SendEmailJob < ApplicationJob
  queue_as :mailers

  # Retry 3 times with exponential backoff, then discard
  retry_on Net::TimeoutError, wait: :polynomially_longer, attempts: 3
  discard_on ActiveRecord::RecordNotFound

  def perform(user_id, template)
    user = User.find(user_id)  # Raises RecordNotFound → discarded
    Mailer.send_template(user, template).deliver_now
  rescue => e
    # Capture unexpected errors to Sentry before re-raising
    Sentry.capture_exception(e, extra: { user_id: user_id })
    raise
  end
end

For failed jobs that exhaust retries, use after_discard hooks or a dead-letter queue strategy to alert the team and preserve the job payload for inspection.

Testing Status Codes in Rails

Rails integration tests and request specs (RSpec) let you assert on response status codes directly:

# RSpec request spec
RSpec.describe 'Users API', type: :request do
  describe 'GET /api/users/:id' do
    context 'when user exists' do
      it 'returns 200 OK' do
        user = create(:user)
        get "/api/users/#{user.id}"
        expect(response).to have_http_status(:ok)
      end
    end

    context 'when user does not exist' do
      it 'returns 404 Not Found' do
        get '/api/users/999'
        expect(response).to have_http_status(:not_found)
        json = response.parsed_body
        expect(json['error']['code']).to eq('not_found')
      end
    end
  end

  describe 'POST /api/users' do
    context 'with invalid params' do
      it 'returns 422 Unprocessable Entity' do
        post '/api/users', params: { user: { email: '' } }
        expect(response).to have_http_status(:unprocessable_entity)
        expect(response.parsed_body['errors']).to be_present
      end
    end
  end
end

Key Rails error handling principles:

  • Use rescue_from for cross-cutting concerns, not inside individual actions
  • Prefer :unprocessable_entity symbol over the raw 422 integer for clarity
  • Use respond_to blocks to serve both HTML and JSON from the same handler
  • Always render your error page templates with the correct status: option — omitting it causes Rails to render your errors/404.html.erb with a 200 OK status

İlgili Protokoller

İlgili Sözlük Terimleri

Daha fazlası Framework Cookbooks