← Blog

Easy error handling in Rails

Let me start by asking what do you think about errors? Many people hate errors and are trying to avoid them as much as possible.

But today I want to show how errors can make your life easier!

Let’s say user submitted some form in your application. Where do you check if it’s valid? In the controller? Controllers should not be concerned with all that is going on in our models. Should the controller ask a model if data is valid or not? The model should already be checking incoming data. What if you introduce some change deep inside your code? You can’t expect to know all the places where it’s used!

In Java World (and in others) errors are not actually called “errors”, they are called “exceptions” and they can help you to handle exceptional situations. Ruby language has similar exception system. Errors can be raised and rescued.

Let’s distinguish between 2 types of errors. 1) Ones that we can actually anticipate and fix. 2) All those bug induced errors, memory errors etc.

First type of error can be fixed by users. Second usually can only be fixed by programmers.

So where should we handle exceptions? Many agree that the most suitable is at system boundaries. We are going to handle it in UI.

First of all, let’s define our new class that can be handled.

class UserError < StandardError
  attr_accessor :data
  def initialize message, data = nil
    @data = data
    super(message)
  end
end

Second, let’s think how we would like our code to look.

# controller
class StuffController < ApplicationController
  def some_action
    handle_exceptions UserError, stuff_page_path(params[:id]) do
      stuff = Stuff.find(params[:id])
      stuff.do_something(params[:x], params[:y])
      render json: { stuff: stuff.in_json }, status: 201
    end
  end
end

# model

class Stuff < ActiveRecord::Base
  def do_something x, y
    if x != y
      raise UserError.new("X has to equal Y!")
    end
  end
end

Now let’s place this method into application controller.

def handle_exceptions *exceptions
  # if nothing is passed then handle the most basic error class Exception
  exceptions << Exception if exceptions.empty?
  begin
    yield # execute code passed into the method
  rescue *exceptions => exception # look for specified exceptions
    # depending on the request format we should perform different actions
    respond_to do |format|
      error_message = exception.respond_to?(:message) ? exception.message : "Some error occurred!"
      format.json do
        render json: { # pass exception's name and a message
                 exception: exception.class.name,
                 message: error_message
               }, status: 400
      end
      format.html do
        # if this request came from some page then redirect back
        # if no request referrer specified then redirect to user's home page
        path = (request.referer && :back) || root_path_for(current_user)
        redirect_to path, alert: error_message
      end
      format.js do
        # if this was Javascript request then do a simplete window.alert
        # window.alert can be replaced with more sophisticated code
        render inline: "window.alert(#{error_message.to_json})".html_safe
      end
    end
  end
end

And make it handle optional redirect path.

def handle_exceptions *exceptions
  # if nothing is passed then handle the most basic error class Exception
  exceptions << Exception if exceptions.empty?
  # if string was passed let's consider it a redirect path
  path = exceptions.select{|e| e.is_a? String}.first
  exceptions.reject!{|e| e.is_a? String}
  begin
    yield # execute code passed into the method
  rescue *exceptions => exception # look for specified exceptions
    # depending on the request format we should perform different actions
    respond_to do |format|
      error_message = exception.respond_to?(:message) ? exception.message : "Some error occurred!"
      # if it's going to help our javascript code let's pass some additional data
      data = exception.respond_to?(:data) ? exception.data : {}
      format.json do
        render json: { # pass exception's name and a message
                 exception: exception.class.name,
                 data: data,
                 message: error_message
               }, status: 400
      end
      format.html do
        # if this request came from some page then redirect back
        # if no request referrer specified then redirect to user's home page
        if path.nil?
          path = (request.referer && :back) || root_path_for(current_user)
        end
        redirect_to path, alert: error_message
      end
      format.js do
        # if this was Javascript request then do a simplete window.alert
        # window.alert can be replaced with more sophisticated code
        render inline: "window.alert(#{error_message.to_json})".html_safe
      end
    end
  end
end

# I always forget if it's errors or exceptions, so let's make it respond to both
alias_method :handle_errors, :handle_exceptions

Let’s see what the JavaScript code may look like (jQuery example):

$.ajax({
	dataType: "json",
	url: "/stuff/" + id + "/do_something",
	data: { x: x, y: y },
})
	.done(function (response) {
		window.alert("Yay!")
	})
	.fail(function (error) {
		// even if no message is provided we should tell a user that something went wrong
		errorMessage = (error.responseJSON && error.responseJSON.message) || "Some error occurred!"
		window.alert(errorMessage)
	})