So you decided that you want use your Rails application as an API, huh? Great! But have you thought of the strategy for error handling? No? That’s also great, because I am here to help you!

Handling errors: the basic approach

If you were trying to find out how to handle error responses in Rails application, the chances are that you found something like this:

  def update
    @article = Article.find(params[:id])

    if @article.update_attributes(article_attributes)
      render json: @article
    else
      render json: @article.errors, status: :unprocessable_entity
    end

  rescue ActiveRecord::RecordNotFound
    render json: { error: error.message }, status: :not_found
  end

This is OK and might be enough, but it quickly gets ugly and disorganized as our logic, edge cases and possible errors to handle grow.

Handling errors behind the scenes

Since most of the endpoints will need some kind of error handling, our goal should be to reduce boilerplate when adding new features. Even more importantly: this will reduce the risk that someone forgets to handle validation errors. The pattern that I want to have is a “happy path” in your action and handling errors behind the scenes:

  def update
    @article = Article.find(params[:id])
    @article.update!(article_attributes)
  end

Moreover, I want to be able to define more error responses easily. There are not too many apps with only 404 and 422 responses.

How to achieve this? I will go through the development process step by step and you can stop as early as you want, even after first step. Every step will work just fine, but the further you go, the more control over error responses and exceptions handling you get.

(In)famous rescue_from

The first step is using Rails’ rescue_from method. While you should be careful using this method, I think it is fairly safe to say that it can make your life much easier as long as it is properly used (and documented…) and as long as your application’s logic fits its usage pattern.

Using exceptions for flow control is sometimes considered as an anti-pattern, but I think that in this case it is really reasonable. The code executed in the rescue_from doesn’t have any side effects and its only result is an immediate error response.

In the Rails documentation you can see that rescue_from lets you define code which will be called when an exception of a given type is raised. Applying this to our example:

class ApplicationController < ActionController::API

  rescue_from ActiveRecord::RecordInvalid, with: :render_unprocessable_entity_response
  rescue_from ActiveRecord::RecordNotFound, with: :render_not_found_response

  def render_unprocessable_entity_response(exception)
    render json: exception.record.errors, status: :unprocessable_entity
  end

  def render_not_found_response(exception)
    render json: { error: exception.message }, status: :not_found
  end
end
class ArticlesController < ApplicationController
  def update
    @article = Article.find(params[:id])
    @article.update!(article_attributes)
  end
end

Done! Now the code in the update action only contains the “happy path” (the article exists and there were no validation errors) and errors are handled in ApplicationController. How does it work? When the find method fails to find a record with the given params[:id] it raises an ActiveRecord::RecordNotFound exception – which is then rescued in ApplicationController by calling the render_not_found_response method. Similarly, using the update! method (with bang) raises an ActiveRecord::RecordInvalid exception when the validation doesn’t pass.

So is it ready? For a very simple application it might be enough, but lets find out what it actualy returns. Example responses in case of 404 and 422 errors:

{
  "error": "Couldn't find Article with 'id'=1"
}
{
  "title": [ "can't be blank" ]
}

Is it OK? If you’re writing a JavaScript application and want to render errors as they are on the page it might be OK. But what if you want to know the type of the validation error and handle processing on the client side? This seems like a reasonable idea, right?

Customizing validation errors

In hobby or small projects you could probably stop here, but when developing most client projects or an open API the chances are that you will have to provide a more specific response format, like this:

{
  "message": "Validation Failed",
  "errors": [
    {
      "resource": "Article",
      "field": "title",
      "code": "blank"
    }
  ]
}

How can we get there? When looking more closely on how rescue works in the case of ActiveRecord::RecordInvalid, you can see that you can call exception.record which returns the problematic record. Now you could just iterate through the errors and construct JSON objects with model name, field and message; it sounds tempting, but in most projects this will lead to one of the following:

  • you will end up with models and attributes naming influencing your API;
  • you will end up with a bunch of ifs to counter the above.

As both of those scenarios sound horrible, I would suggest making a mechanism encouraging other developers to write new code in a proper way (rather then leaving them with a temptation to write “just another small if” to change the resource or field name).

We will need a class which will handle conversion from standard Rails messages to exactly what we want:

class ValidationErrorsSerializer

  attr_reader :record

  def initialize(record)
    @record = record
  end

  def serialize
    record.errors.details.map do |field, details|
      details.map do |error_details|
        ValidationErrorSerializer.new(record, field, error_details).serialize
      end
    end.flatten
  end
end

The thing to notice here is the errors.details method. Before Rails 5 the only way to get the error code name (e.g. blank, rather than the full message like can't be blank) was to hack Rails a bit by using the translation_metadata method. It is not funny to read I18n code to find this, but even if you are not running Rails 5 yet, there is a way to use the details method because it was extracted to a gem (which is really great!).

Besides that, the class is fairly simple. record.details returns a hash with keys being attribute (field) names and values being arrays of hashes with details for each error, e.g. {name: [{error: :blank}, {error: :too_short}]}. The ValidationErrorsSerializer just calls ValidationErrorSerializer for every such hash, additionally passing the record and the field name.

The core logic is in ValidationErrorSerializer:

class ValidationErrorSerializer

  def initialize(record, field, details)
    @record = record
    @field = field
    @details = details
  end

  def serialize
    {
      resource: resource,
      field: field,
      code: code
    }
  end

  private

  def resource
    I18n.t(
      underscored_resource_name,
      scope: [:resources],
      locale: :api,
      default: @record.class.to_s
    )
  end

  def field
     I18n.t(
      @field,
      scope: [:fields, underscored_resource_name],
      locale: :api,
      default: @field.to_s
    )
  end

  def code
    I18n.t(
      @details[:error],
      scope: [:errors, :codes],
      locale: :api,
      default: @details[:error].to_s
    )
  end
  
  def underscored_resource_name
    @record.class.to_s.gsub('::', '').underscore
  end
end

Pretty good, don’t you think? This class is very simple, but it is quite clever as well. Normally, you would use the I18n.t method (read more about I18n here) to define translations for different languages. In this case it is a convenient way to have custom model and attribute names in the case where you want to return in the API something different from what’s in your database schema (which is often what should happen).

Each I18n.t call accepts two parameters to achieve that:

  • locale: passing :api ensures that the api.yml file will be used,
  • default: the default value (in the case when there is no entry in the api.yml file for the given model, attribute or code).

An example api.yml file could look like this:

api:
  resources:
    user: author
  fields:
    article:
      name: title
  errors:
    codes:
      blank: cant_be_blank

On Rails 5 you might get I18n::InvalidLocale exception, which is raised when a locale is not on the list of available locales. For previous Rails versions config.i18n.enforce_available_locales is set to false by default. This can be fixed by either setting the list of available locales or ignoring locales validation in your application config:

config.i18n.available_locales = [:en, :api]
# or
config.i18n.enforce_available_locales = false

The only thing left to make this work is updating the render_unprocessable_entity_response method:

def render_unprocessable_entity_response(exception)
  render json: {
    message: "Validation Failed",
    errors: ValidationErrorsSerializer.new(exception.record).serialize
  }, status: :unprocessable_entity
end

Customizing other errors

I promised that besides having much less boilerplate, you will be able to define custom error responses easily. This is actually very simple to code; we will only need some basic Ruby features.

When looking at class hierarchy of error classes in Rails, one noticable thing is that all errors inherit from StandardError. This means that we can define such class ourselves and use it whenever we need to return custom error responses.

Let’s say that we expect the following format for error responses:

{
  "message": "Not found",
  "code": "not_found"
}

or

{
  "message": "User doesn't have admin rights.",
  "code": "not_an_admin"
}

In the case of the 404 Not found error, returning the code and message may not make that much difference (since the 404 status of an HTTP request is the only indication you need), but in the case of the 403 Forbidden response (like in the second example) we may want to have different response messages.

Since 404 Not found will be returned for ActiveRecord::RecordNotFound, no new class is needed for this kind of exception, but for the admin example I suggest the following class:

class UserIsNotAnAdmin < StandardError

  def http_status
    403
  end

  def code
    'not_an_admin'
  end

  def message
    "User doesn't have admin rights."
  end

  def to_hash
    {
      message: message,
      code: code
    }
  end
end

Applying this to the current codebase:

class ApplicationController < ActionController::Base

  rescue_from ActiveRecord::RecordInvalid, with: :render_unprocessable_entity_response
  rescue_from ActiveRecord::RecordNotFound, with: :render_not_found_response
  rescue_from UserIsNotAnAdmin, with: :render_error_response

  def render_unprocessable_entity_response(exception)
    render json: {
      message: "Validation Failed",
      errors: ValidationErrorsSerializer.new(exception.record).serialize
    }, status: :unprocessable_entity
  end

  def render_not_found_response
    render json: { message: "Not found", code: "not_found" }, status: :not_found
  end
  
  def render_error_response(exception)
    render json: { message: exception.message, code: exception.code }, status: exception.http_status
  end
end

Now whenever you want the API to return a 403 response when the user doesn’t have admin rights, all you need to do is write:

def do_something
  raise UserIsNotAnAdmin unless user.admin?
  # do something
end

When UserIsNotAnAdmin is raised, the code execution is immediately stopped and the exception is rescued by calling render_error_response. Notice that this method has a generic name (instead of, e.g., render_not_an_admin_response). The rescue_from method can receive any number of class names; since render_error_response doesn’t really care about what object is passed (as long as it implements the message and code methods), we can pass any other class there, like:

    rescue_from UserIsNotAnAdmin,
                AuthenticationFailed,
                AuthenticationAttemptsExceeded,
                with: :render_error_response

Is there anything more to be done?

Yes, one thing: testing. I didn’t write anything about testing APIs, but I am preparing a blog post about testing Rails applications in which I will use a similar error handling approach. Other than that, the cases described above cover most of Rails applications I’ve seen in my career. That doesn’t mean every single one will fit, though!