ruby
46 lines · 7 steps
Centralized API error handling in Rails
A base controller maps common exceptions to consistent JSON error responses so every endpoint fails the same way.
Explained by
highlit
1module Api
2 module V1
3 class BaseController < ActionController::API
4 rescue_from StandardError, with: :render_internal_error
5 rescue_from ActiveRecord::RecordNotFound, with: :render_not_found
6 rescue_from ActiveRecord::RecordInvalid, with: :render_unprocessable
7 rescue_from ActionController::ParameterMissing, with: :render_bad_request
8 rescue_from Pundit::NotAuthorizedError, with: :render_forbidden
9
10 private
11
12 def render_not_found(error)
13 render_error(status: :not_found, code: "not_found", message: error.message)
14 end
15
16 def render_unprocessable(error)
17 render_error(
18 status: :unprocessable_entity,
19 code: "validation_failed",
20 message: "Validation failed",
21 details: error.record.errors.map { |e| { field: e.attribute, message: e.message } }
22 )
23 end
24
25 def render_bad_request(error)
26 render_error(status: :bad_request, code: "bad_request", message: error.message)
27 end
28
29 def render_forbidden(_error)
30 render_error(status: :forbidden, code: "forbidden", message: "You are not authorized to perform this action")
31 end
32
33 def render_internal_error(error)
34 Rails.logger.error("[#{request.request_id}] #{error.class}: #{error.message}")
35 Rails.logger.error(error.backtrace.take(20).join("\n"))
36 render_error(status: :internal_server_error, code: "internal_error", message: "Something went wrong")
37 end
38
39 def render_error(status:, code:, message:, details: [])
40 payload = { error: { code: code, message: message } }
41 payload[:error][:details] = details if details.any?
42 render json: payload, status: status
43 end
44 end
45 end
46end
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Registering rescue_from handlers in a base controller gives every subclass uniform error responses for free.
- 2Order matters: specific exceptions before the StandardError catch-all so they don't get swallowed.
- 3A single render helper keeps the error payload shape consistent across every failure case.
Related explainers
ruby
module Multitenant extend ActiveSupport::Concern included do
How a multitenant concern scopes Rails models
multitenancy
default-scope
concern
Advanced
8 steps
ruby
require "openssl" require "base64" module WebhookSignature
Signing and verifying webhooks in Ruby
hmac
cryptography
webhooks
Intermediate
7 steps
javascript
import { parsePhoneNumberFromString } from 'libphonenumber-js'; export class PhoneValidationError extends Error { constructor(message, code) {
Normalizing phone numbers with typed errors
validation
error-handling
custom-errors
Intermediate
6 steps
typescript
import { Injectable, computed, inject, signal } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { firstValueFrom } from 'rxjs';
Optimistic updates with Angular signals
signals
optimistic-updates
state-management
Intermediate
9 steps
python
import os from datetime import datetime, timezone import jwt
JWT bearer auth as a FastAPI dependency
authentication
jwt
dependency-injection
Intermediate
7 steps
rust
use std::error::Error; use std::path::Path; use serde::Deserialize;
Custom CSV deserialization with serde in Rust
serde
csv
deserialization
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/centralized-api-error-handling-in-rails-explained-ruby-1e61/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.