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.
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.
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?
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
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.
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.
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
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
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.
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 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,…
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,…