Consistently Handle Errors with a ModelErrors Concern

Consistently Handle Errors with a ModelErrors Concern

·

2 min read

💡
Trigger Warning! This approach may be considered controversial, especially since it touches on using exceptions for control flow.

This is a pattern you can use in your Rails projects to keep your controllers lean. It works especially well in Rails API projects, but I think you can find success with it even when using HTML over the wire. I originally found this while searching through the Rails API docs on the Rescuable concern.

The idea is to use the rescue_from behavior in controllers to render a consistent error response for any controller that works with an active record model, or active model model.

Usually we always write that standard:

def create 
  @blog_post = BlogPost.new(...)
  if @blog_post.save
    # render ....
  else
    render :new
  end
end

Instead use the bang version of save! (or validate!, in the case of an ActiveModel::Model), and add the corresponding rescue_from call:

def create 
  @blog_post = BlogPost.new(...)
  @blog_post.save!
  render ....
end

rescue_from ActiveRecord::RecordInvalid, with: :model_errors
rescue_from ActiveModel::ValidationError, with: :model_errors

def model_errors(error)
  # Depending on the type of error, the errors will be either on the model/record property
  errors = error.respond_to?(:record) ? error.record.errors : error.model.errors
  respond_to do |format|
    format.json { json: { errors: errors.full_messages }, status: :unprocessable_entity } }
  end
end

For API controllers, this helps enforce consistency in response format/status code, in addition to DRYing up your code.

For a new codebase, you could potentially put the code in ApplicationController, but in a more mature codebase, you might consider using a concern where you can opt controllers into this behavior on a one-by-one basis, confirming you don’t unintentionally cause regressions:

# app/controllers/concerns/model_errors.rb
module ModelErrors
  extend ActiveSupport::Concern

  included do
    rescue_from ActiveRecord::RecordInvalid, with: :model_errors
    rescue_from ActiveModel::ValidationError, with: :model_errors
  end

  def model_errors(error)
    # Depending on the type of error, the errors will be either on the model/record property
    errors = error.respond_to?(:record) ? error.record.errors : error.model.errors
    respond_to do |format|
      format.json { json: { errors: errors.full_messages }, status: :unprocessable_entity } }
    end
  end
end

If you’re interested in a similar approach when sending HTML over the wire, here’s a GitHub Gist with a sample implementation (using a tidbit more logic to get the render calls right):

https://gist.github.com/mgodwin/4db6ec1eaca5d08243bada38bf02360b

Â