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_fromfor cross-cutting concerns, not inside individual actions - Prefer
:unprocessable_entitysymbol over the raw422integer for clarity - Use
respond_toblocks 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 yourerrors/404.html.erbwith a200 OKstatus