JSON API Errors Handler - a new way of catching API exceptions in ruby

When some time ago I came with an advanced way of handling exceptions in Ruby Applications I realized, that this is a wider problem than I thought. A lot of people found the idea very useful and therefore I decided to go with this topic even further.

JSON API Errors Handler

The natural follow-up is a JsonapiErrorsHandler gem. This is a small, open-source package that you can hook into any kind of Ruby or Rails application and it helps you handle API errors in the really convenient way.

How does it work?

The usual error handling in rails applications is well described in my previous post, so let's focus here on how the gem's behavior makes it easier for you to work with API errors.

The main idea is to support JSON API standard for your API communication. Whenever you'll raise an error in your application while processing the HTTP request, the JsonapiErrorsHandler will handle that fact and remove the overhead from you by delivering the well-formatted response to the client.


JsonapiErrorsHandler gem's concept  

 

  


As you can see, it's fairly simple. The idea is just to remove the responsibility to recognize serializable errors and the logic of translating it into JsonAPI-friendly format out of your application.

Using JsonapiErrorsHandler gem.

To use the gem in the rails application, just go to your base API controller, and include those two lines at the top:

# app/controllers/api/application_controller.rb

...
include JsonapiErrorsHandler

rescue_from ::StandardError, with: lambda { |e| handle_error(e) }
...

By default, gem defines 5 types of errors that, when risen, are handled and transformed out of the box:

  • JsonapiErrorsHandler::Errors::Unauthorized => status: 401
  • JsonapiErrorsHandler::Errors::Forbidden => status: 403
  • JsonapiErrorsHandler::Errors::NotFound => status: 404
  • JsonapiErrorsHandler::Errors::Invalid => status: 422
  • JsonapiErrorsHandler::Errors::StandardError => status: 500

If you want to return a friendly error message, just raise any of the errors above in your controller.

def show
  raise JsonapiErrorsHandler::Errors::NotFound unless ... # do the fancy find
end

That's already looking pretty cool, but we can do even better.

JSON API Error Mapper

in rails applications, a very common way to find resources is by using the find method. If you do so, in case of failure, there is already a default Actvierecord::RecordNotFound exception risen. In such cases, Our code would not be too pretty, would it?

def show
  @user = User.find(params[:id])
rescue ActiveRecord::RecordNotFound
  raise JsonapiErrorsHandler::Errors::NotFound
end

In such cases, the custom ErrorMapper comes handy.

In JsonapiErrorsHandler you can have ANY error being mapped into any API-friendly error. Let's refactor our code above to make use of this feature.

# app/controllers/api/application_controller.rb

...
include JsonapiErrorsHandler
JsonapiErrorMapper.map_errors!({
  'ActiveRecord::RecordNotFound' => 'JsonapiErrorsHandler::Errors::NotFound'
})
rescue_from ::StandardError, with: lambda { |e| handle_error(e) }
...

Here in the application controller after including the JsonapiErrorsHandler gem we added an error mapping call. This way we informed the gem that whenever the error of a class specified in the key of the mapper is being risen, handle it using an instance of a class specified as a value.

This allows us to completely get rid of error's handling overhead from all controllers in our application:

def show
  @user = User.find(params[:id])
end


Using JsonapiErrorsHandler in non-rails applications

If you'll look into the gem's repository, currently there is only one line that kind of dependents of rails, and it's a method: render_error:

def render_error(error)
  render json: ::JsonapiErrorsHandler::ErrorSerializer.new(error), status: error.status   end

Here we use render function which is rails-specific. If you want to use the gem in Sinatra, Hanami, or whatever else, just be sure you take this fact into the account. Either:

  1. Override a render_error method in the class that includes the module
  2. implement the render method, so the including class will have this accessible.

Drawbacks

Help welcome

Currently, this gem was tested only with Rails applications, but it's designed in the way there should not be too much rails dependencies involved. There is still room to improve So ANY contributions are very welcome!

Next steps

For this gem, I'll work on mutation test coverage, improved code quality and reduce the dependency of any particular framework. Also, I plan to come with a few more useful API gems so If you have any annoying problems in your Ruby applications, let me know in the comments.

Special thanks

  1. Tamara Bellis for a great cover photo.
  2. Nguyễn Trịnh Hồng Ngọc for a very early contribution to the gem and overall support.
  3. All students of the Ruby on Rails REST API course who helped to make this gem real.