Introduction
APIs need consistent, machine-readable error responses. Every error — validation failure, not found, unauthorized, server error — should follow the same JSON structure so clients can handle errors generically.
Key Concepts
- Error Envelope: A consistent JSON structure for all errors (e.g.,
{ error: { message, status, code } }). rescue_from: A Rails method that catches exceptions and renders custom responses.- Problem Details (RFC 7807): A standard for HTTP API error responses.
- Validation Errors: Errors returned when model validation fails.
Real World Context
APIs that return inconsistent errors are painful to consume. Stripe, GitHub, and Twilio all use consistent error formats. A standard format means clients can handle all errors with a single error-handling function.
Deep Dive
Application-Level Error Handling
rubyclass ApplicationController < ActionController::API rescue_from ActiveRecord::RecordNotFound, with: :not_found rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity rescue_from ActionController::ParameterMissing, with: :bad_request private def not_found(exception) render json: { error: { status: 404, message: "Resource not found", detail: exception.message } }, status: :not_found end def unprocessable_entity(exception) render json: { error: { status: 422, message: "Validation failed", errors: exception.record.errors.full_messages } }, status: :unprocessable_entity end def bad_request(exception) render json: { error: { status: 400, message: "Bad request", detail: exception.message } }, status: :bad_request end end
Every error follows the same shape: status, message, and additional details. Clients parse error.message for display and error.status for programmatic handling.
Validation Error Details
rubydef create product = Product.new(product_params) if product.save render json: product, status: :created else render json: { error: { status: 422, message: "Validation failed", errors: product.errors.messages } }, status: :unprocessable_entity end end
errors.messages returns a hash like { name: ["can't be blank"], price: ["must be greater than 0"] }, letting clients show field-specific errors.
Common Pitfalls
- Leaking stack traces in production — Never include exception backtraces in API responses. Use generic messages for 500 errors.
- Inconsistent error formats — If some endpoints return
{ message: "..." }and others return{ error: "..." }, clients need special handling.
Best Practices
- Handle errors at the application level —
rescue_fromin ApplicationController catches errors from all endpoints. - Return field-specific validation errors —
errors.messageslets clients highlight specific form fields.
Summary
- Use a consistent error envelope for all API errors.
rescue_fromcatches exceptions at the application level.- Include field-specific validation errors for form handling.
- Never expose stack traces or internal details in production.
Code Examples
ruby
class ApplicationController < ActionController::API
rescue_from ActiveRecord::RecordNotFound do |e|
render json: {
error: { status: 404, message: "Not found", detail: e.message }
}, status: :not_found
end
rescue_from ActiveRecord::RecordInvalid do |e|
render json: {
error: { status: 422, message: "Validation failed",
errors: e.record.errors.messages }
}, status: :unprocessable_entity
end
end