Error Handling
Define business errors in contracts and handle them in controllers
Error Handling
Zodra has three layers of error handling: param validation, business errors, and exception mapping.
Defining errors in contracts
Declare possible errors for each action:
Zodra.contract :orders do
action :confirm do
params do
uuid :id
end
response :order
error :not_found, status: 404
error :invalid_transition, status: 422
end
endEach error has:
code— a symbolic identifier (becomes a string in the response)status:— the HTTP status code
Param validation errors
When request params fail validation (wrong type, missing required field, value out of range), Zodra automatically responds with a 422 status and field-level errors:
{
"errors": {
"name": ["must be at least 1 character"],
"price": ["must be greater than or equal to 0"]
}
}No additional code needed — this is handled by zodra_params in the controller.
Validation error keys
By default, Zodra derives valid error keys from your params definition. You can also declare them explicitly with the errors block:
Zodra.contract :products do
action :create do
params do
string :name, min: 1
string :sku, min: 1
money :price, min: 0
end
response :product
errors do
from_params
key :base
end
end
endfrom_params
Generates error keys from the action's params definition. Supports except: to exclude specific keys:
errors do
from_params except: [:internal_id]
key :base
endNested error keys
For actions with nested data (arrays of objects), declare nested keys with a block:
action :create do
params from: :order_input
response :order
errors do
key :base
key :customer_id
key :shipping_address
key :items do
key :product_id
key :quantity
end
end
endZodra validates error keys at all nesting levels. In non-production environments, unknown keys raise an error with the full path:
Unknown error keys [:bad_key] in items[0] for action :create.In production, unknown keys are logged as warnings instead.
Business errors with zodra_rescue
Map Ruby exceptions to error codes declared in your contract:
class OrdersController < ApplicationController
include Zodra::Controller
zodra_rescue :confirm, InvalidTransitionError, as: :invalid_transition
zodra_rescue :cancel, InvalidTransitionError, as: :invalid_transition
def confirm
order = Order.find(zodra_params[:id])
order.confirm!
zodra_respond(order.reload)
end
endWhen InvalidTransitionError is raised during confirm, Zodra responds with:
{
"error": {
"code": "invalid_transition",
"message": "Order cannot be confirmed in its current state"
}
}The HTTP status is taken from the error definition in the contract (422 in this case).
Manual error responses
Use zodra_errors for field-level errors that aren't from param validation:
def create
product = Product.new(zodra_params)
if product.save
zodra_respond(product, status: :created)
else
zodra_errors(product.errors)
end
endzodra_errors accepts ActiveModel::Errors objects, hashes, or anything responding to .messages. Keys are automatically transformed to match your key_format configuration (e.g., snake_case to camelCase), including nested keys in arrays.
ErrorMapper
For complex scenarios where errors come from multiple sources (parent record, child records, external services), use Zodra::ErrorMapper:
class OrderErrorMapper < Zodra::ErrorMapper
def call(order:)
collect(order) do
ignore :number, :status, :total_amount
map_remaining
end
item_errors = build_item_errors(order.line_items)
result[:items] = item_errors if item_errors.any?
result
end
private
def build_item_errors(line_items)
line_items.filter_map do |line_item|
next if line_item.errors.empty?
mapped = {}
line_item.errors.to_hash.each do |key, messages|
next if %i[order unit_price total_price].include?(key)
mapped_key = key == :product ? :product_id : key
mapped[mapped_key] = messages
end
mapped.presence
end
end
endUse it in the controller:
def create
order = build_order_record
if order.valid? && order.line_items.all?(&:valid?)
order.save!
zodra_respond(order.reload, status: :created)
else
zodra_errors(OrderErrorMapper.call(order: order))
end
endErrorMapper API
Inside a collect block, these methods are available:
| Method | Description |
|---|---|
map(from => to) | Remap a key (e.g., map product: :product_id) |
map_remaining | Transfer all unmapped keys as-is |
ignore(*keys) | Skip keys from the source |
consume(key) | Extract and remove a key from the source |
add(key, message) | Manually add an error message |
source_errors | Access the raw source error hash |
assert_no_unmapped! | Raise if any source has unmapped keys (strict mode) |
Client-side error handling
The @zodra/client parses error responses into typed error classes. See Client Error Handling.