← Blog
Easy error handling in Rails
- Programming
- Ruby on 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)
})