March 27th, 2013 by Brent

Raising and Rescuing Custom Errors in Rails

Following on from our post on Dynamic Error Page in Rails, this week we're going to look at raising and rescuing custom errors in a Rails application.

It's often useful to map custom Ruby errors to HTTP response status codes and have Rails render the appropriate HTML error pages. For example, you might have a controller that is acting as a simple proxy to a third party service such as Twitter or Facebook, and you need any of the HTTP errors encountered when calling those sites to be handled natively by your app. Another use case would be in a Service-oriented architecture (SOA), where you want any errors in your back end services propagated to your front end web application.

In this post we'll demonstrate rescuing status errors in an imaginary proxy controller using the awesome Faraday gem. For the sake of brevity we've omitted the inclusion of tests though in the wild we'd build such a feature using TDD and our favourite test weapon, RSpec.

Not Found

To start, let's handle basic 404 Not Found errors that occur when calling a service. For this we'll need a custom error class that extends StandardError.

# lib/errors/not_found.rb
module Errors
  class NotFound < StandardError; end
end

Faraday provides a neat Rack-esque middleware feature. By creating our own custom middleware we can catch any Faraday 404s and raise our custom error. Furthermore, we can re-use the middleware anytime we need the same behaviour.

# lib/errors/raise_error.rb
module Errors
  class RaiseError < Faraday::Response::Middleware

    def on_complete(env)
      raise Errors::NotFound if env[:status] == 404
    end

  end
end

Now for the proxy controller.

# app/controllers/proxy_controller.rb
class ProxyController < ApplicationController

  def index
    connection = Faraday.new(:url => 'http://someservice') do |f|
      f.adapter Faraday.default_adapter
      f.use     Errors::RaiseError       # Include custom middleware
    end

    response = connection.get('/some/resource')

    render :text => response.body
  end

end

At this point any NotFounds raised will still result in a 500 Internal Server Error in Rails. To alleviate this let's create a module that uses rescue_from, catches any custom NotFounds and renders the default 404 page.

# lib/errors/rescue_error.rb
module Errors
  module RescueError

    def self.included(base)
      base.rescue_from Errors::NotFound do |e|
        render "public/404", :status => 404
      end
    end

  end
end

We can then mixin RescueError into our application controller and handle NotFounds app-wide.

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include Errors::RescueError
end

Unprocessible Entity and Internal Server Error

Next, let's create custom errors to help us manage proxy 422s and 500s.

# lib/errors/internal_server_error.rb
module Errors
  class InternalServerError < StandardError; end
end

# lib/errors/unprocessable_entity.rb
module Errors
  class UnprocessableEntity < StandardError; end
end

We'll also need to modify the RaiseError middleware to raise those errors. Here, we also ignore any non-error response codes, and treat any unknown error responses as 500s.

# lib/errors/raise_error.rb
module Errors
  class RaiseError < Faraday::Response::Middleware
    def on_complete(env)
      # Ignore any non-error response codes
      return if (status = env[:status]) < 400
      case status
      when 404
        raise Errors::NotFound
      when 422
        raise Errors::UnprocessableEntity
      else
        raise Errors::InternalServerError # Treat any other errors as 500
      end
    end
  end
end

Now, let's change the RescueError implementation to render the appropriate HTML page.

# lib/errors/rescue_error.rb
module Errors
  module RescueError

    def self.included(base)
      base.rescue_from Errors::NotFound do |e|
        render "public/404", :status => 404
      end
      base.rescue_from Errors::UnprocessableEntity do |e|
        render "public/422", :status => 422
      end
      base.rescue_from Errors::InternalServerError do |e|
        render "public/500", :status => 500
      end
    end

  end
end

Refactoring

So far so good. The code does what we need but there's far too much duplication. Furthermore, if we want to map additional error codes we'll have to add more branches to the switch statement in RaiseError and more rescue_from handlers to the RescueError class.

Accordingly, it's time to refactor the existing code. Firstly, let's delete our existing StandardError subclasses, and create the equivalent classes using a sprinkling of metaprogramming.

# lib/errors.rb
module Errors

  # Error constants
  NOT_FOUND               = 404
  UNPROCESSABLE_ENTITY    = 422
  INTERNAL_SERVER_ERROR   = 500

  # Base subclass for all response errors
  class ResponseError < StandardError; end

  class << self

    # Returns a hash of error names to response codes.
    def error_constants
      self.constants.each_with_object({}) do |name, hash|
        # Ignore any class constants
        next if (code = Errors.const_get(name)).is_a?(Class)
        hash[name] = code
      end
    end

    # Returns a class name from a constant name.
    def class_name_for_error_name(name)
      name.to_s.titleize.gsub(' ', '')
    end

    # Returns the error class for a given constant name.
    # Default to InternalServerError.
    def class_for_error_name(name)
      class_name = class_name_for_error_name(name)
      const_defined?(class_name) ? Errors.const_get(class_name) : Errors::InternalServerError
    end

    # Returns the error class for a given error code.
    # Default to InternalServerError.
    def class_for_error_code(code)
      name = error_constants.select { |k, v| v == code }.keys.first
      name.present? ? class_for_error_name(name) : Errors::InternalServerError
    end

  end

end

# Dynamically creates a subclass of ResponseError for each error constant.
# Adds a code method to return the correct response code.
# Adds the new class to the constants in the Errors module.
Errors.error_constants.each do |name, code|
  klass = Class.new(Errors::ResponseError)
  klass.send(:define_method, :code) { code }
  Errors.const_set(Errors.class_name_for_error_name(name), klass)
end

This is a large refactoring, but permits us to vastly simplify the error raising code.

# lib/errors/raise_error.rb
module Errors
  class RaiseError < Faraday::Response::Middleware

    def on_complete(env)
      return if (status = env[:status]) < 400
      raise Errors.class_for_error_code(status)
    end

  end
end

And similarly, the error rescuing code.

# lib/errors/rescue_error.rb
module Errors
  module RescueError

    def self.included(base)
      base.rescue_from Errors::ResponseError do |e|
        render "public/#{e.code}", :status => e.code
      end
    end

  end
end

Now, raising and rescuing additional error types is as simple as adding new constants to the errors.rb file.

# lib/errors.rb
module Errors

  # Error constants
  BAD_REQUEST             = 400
  NOT_AUTHORIZED          = 401
  FORBIDDEN               = 403
  NOT_FOUND               = 404
  METHOD_NOT_ALLOWED      = 405
  BAD_FORMAT              = 406
  UNPROCESSABLE_ENTITY    = 422
  INTERNAL_SERVER_ERROR   = 500

  ...
end

Summary

This brings our recent posts on error handling in Rails to a close. Feel free to add any suggestions, questions or feedback in the comments section.