How to handle exceptions in Rails—A Practical Guide
<  Go to blog home page

How to handle exceptions in Rails—A Practical Guide


We often encounter exceptions in Ruby on Rails, that if we don’t manage correctly from the beginning, can cause problems and time waste. But solving them doesn’t have to be so tedious! This article provides an overview of how to handle exceptions in Ruby on Rails, helping you implement strategies and best practices to do it efficiently.

The problem with Ruby on Rails exceptions

Picture this: you’re working with some code that needs to make many different validations and you should return specific error messages or even specific status codes for each case. Let’s say we are making a user signup validation that will check if the email and the password are present it would look like this:

def validate!(user)
	if user[:email] && user[:password]
		puts "Great the user is present"
	else
		raise "Some attribute is missing but we don't know which"
	end
end

while the exception handler at the ApplicationController would look like the following:

class ApplicationController < ActionController::Base
	rescue_from StandardError, with: :handle_errors

	def handle_errors(e)
		render json: { message: e.message }, status: 500
	end
end

Okay let’s be honest—in the example above, we are not being very specific about what are we missing. So, let’s make a couple changes to improve it.

Initial changes to produce clearer messages for Ruby on Rails exceptions

The following is a better alternative to what we saw before:

def validate(user)
	if !user[:email]
		raise "Email is not present"
	else if !user[:password]
		raise "Password is not present"
	end

	puts "Great the user is present"
end

With this, we would be returning clearer error messages. But there’s still one issue with the code above, the handler is always sending a 500 status which could apply for some errors but not for the ones that we are throwing, in this case it would be better to respond with 400 that states for Bad Request, 406 that states for Not Acceptable or even 422 for an Unprocessable Entity, and at this point we have some options to take, one of them being defining our own exceptions like in the following example:

class InvalidUser < StandardError
end

Which could be then implemented like this

# ApplicationController
rescue_from InvalidUser do |e|
	render json { message: e.message }, status: 422
end

# Validate user
def validate(user)
	error = nil
	if !user[:email]
		error = "Email is not present"
	elsif !user[:password]
		error = "Password is not present"
	end

	return InvalidUser.new(error) if error

	puts "Great the user is present"
end

Well, that looks way better. But what happens when we need to add many different exception classes?

Why we need to go a step further to handle exceptions in Ruby on Rails

We need to be more granular about what to do if a user transaction fails and know how to handle a rollback action in a exception handler, or we would end with tons exception class files in the project and with a lot of different handlers in the top of the application.

# ApplicationController
rescue_from InvalidUser #...
rescue_from InvalidTransaction #...
# Project structure
| lib
|-- exceptions
|--|-- invalid_user.rb
|--|-- invalid_transaction.rb
|--|-- etc...

As defining the class also implies creating the file and modifying the handlers, sometimes it’s easier for a team to not define any and return generic errors instead or even defining the exceptions in the same class of the service that is going to use them, which just increases the complexity of the class and is not convenient for the cases when we need to reuse the exception, because we would need to access it through the service as ValidateUserService::InvalidUser which would also increase the coupling between the classes, and would make it harder to refactor.

class ValidateUserService
class InvalidUser < StandardError
end
# ...
end

The solution

In order to reduce the project files, and defining a framework for adding exceptions we can create a file which is going to store the configuration of each exception, that will contain the message, http_code, and any other information we find useful for debugging or for context, for example I like to add an internal code that could help for communication between a CS and a development team.

Defining the configuration

For this I’m going to use yaml (this is a personal preference because it doesn’t need neither brackets nor quotation marks) but you can use any other type of file that can be parsed as a hash in ruby.

invalid_user:
	message: The user is not valid
	status: !ruby/symbol unprocessable_entity
	http_code: 422
	code: 10001
invalid_transaction:
	message: The transaction is not valid
	status: !ruby/symbol not_acceptable
	http_code: 406
	code: 10002

Now we need to define a class that will act as the intermediary between the definition file and our code, it will be called CustomException but you can use any name that results more convenient for your case.

Exceptions entrypoint

As I’m using yaml, I added this gem to parse it into a hash:

class CustomException < StandardError
	EXCEPTIONS_PATH = './exceptions.yaml'

  attr_accessor :config, :status, :code, :message, :http_code, :errors

  def initialize(config)
	  # set the config object for debugging purposes
    @config = config
    set_defaults(config)
  end

  def set_defaults(config)
    @code = config['code']
    @status = config['status']
    @message = config['message']
    @http_code = config['http_code']
    @errors = config['errors'] || [@message]
  end

This will be used in the exception handler to return a custom status

  def response_objec
{
code: code,
      status: status,
      message: message,
      errors: errors,
      http_code: http_code
    }
  end
  class << self
  # This will be called each time that a non defined class is invoked
    def const_missing(name)
undesrcored_const = name.to_s.underscore
raise "Exception Not Defined" unless exists?(undesrcored_const)
err_hash = exceptions[undesrcored_const]
# Here we define the new class as a child of CustomException
      const_set(name, Class.new(CustomException) do
        define_method :initialize do |errors = nil|
          super(err_hash.merge("errors" => errors))
        end
      end)
    end
def exists?(name)
exceptions.include?(name)
end
# Here we are reading the exception definitions from the config file
def exceptions
@exceptions ||= File.read(EXCEPTIONS_PATH).yield_self do |f|
YAML.load(f) || {}
end
rescue StandardError
{}
end
  end
end

With this we are set for our first exceptions which will be created dynamically defined when called, that can be raised and handled in the following way, returning to the user validation that we mentioned earlier in this article.

# ValidateUserService
def validate
	# ...
	raise CustomException::Invaliduser if error
	# ...
end

# ApplicationController
class ApplicationController < ActionController::Base
	rescue_from CustomException do |e|
		render json { message: e.message, code: e.code }, status: e.status
	end
end

Improvements

This solution can also be implemented with i18n which will help you to return translated messages for the different languages that your platform uses, you would only need to replace the configuration file for i18n and modify the const_missing  method like this:

def const_missing(name)
	const_set(name, Class.new(CustomException) do
		define_method :initialize do |errors = nil|
			I18n.reload!
			I18n.locale = :en
			err_hash = I18n.t("custom_exception.#{name.to_s.underscore}")
			super(err_hash.merge(errors: errors))
		end
	end
end

The original idea behind the errors attribute is to provide more context about the errors, for example, having only one InvalidUser exception with the message The User is invalid and accessing the error attribute to find the Missing Email or Missing Password

Final thoughts

I hope you find this guide useful. But always remember, no solution is perfect or fits everywhere. Before implementing this, make sure that it does not interfere with the practices and standards of your organization or your team. We will be reviewing more topics related to metaprogramming in the next articles.

Explore our next posts

Integrating a LATAM remote software development team
Management

Integrating a LATAM remote software development team

Today, numerous businesses choose to augment their software development team with remote developers for a variety of advantages, including cost-effectiveness, experienced personnel, and quicker product launch. Latin America is an increasingly popular choice for North American companies due to its abundant talent pool, comparable time zones, and shared cultural values. This article provides tips and

A Machine Learning Starter Guide For 2023
Technology

A Machine Learning Starter Guide For 2023

A lot is being said about Machine Learning. However, as much as Machine Learning is thought to be one of the 21st century’s most important achievements (even if everything actually started in the early 20th century), by many, even tech experts, it still looks like fruit that is hanging in the top of the tree,

How to tackle an upcoming shortage of cost-effective developers
Management

How to tackle an upcoming shortage of cost-effective developers

The demand to hire affordable top talented developers has increased significantly in recent years, and this trend is set to continue in the near future. However, with the onset of a recession, companies may struggle to find cost-effective developers to meet their needs. This is where the advantages of outsourcing software development become more relevant,

Join BEON.tech's community today

Apply for jobs Hire developers