DEV Community

Jose Aguilar
Jose Aguilar

Posted on

Exception Handling and Validations in Rails, and how to display errors to users.

Introduction

In this article we will go over exception handling and validation in Rails. Exception handling and validation are crucial for any web application, including those built with Rails; it's how we're able to display error messages that are useful not only to us as developers, but to our application users as well. How would you know if you entered the wrong username or password in an application without any validation or error handling?

First, a refresher(or introduction) on exceptions if you need it. Otherwise, skip ahead to the Validation section!


What is an exception?

You've already seen exceptions in action. For example, if you've ever made a typo in your code causing your program to crash with 'NoMethodError' or 'SyntaxError', you're seeing an exception being raised:

putss 'Hello World'
# => NoMethodError: undefined method 'putss' for main:Object
# => Did you mean? puts
Enter fullscreen mode Exit fullscreen mode

An exception represents an error condition in a program. Exceptions are how Ruby deals with unexpected events, and they provide a mechanism for stopping the execution of a program. When an exception is raised, your program starts to shut down. If nothing halts that process, your program will eventually stop and print out an error message.

Exception Handling

Crashing programs are no bueno. Normally we want to stop the program from crashing and react to the error. This is called "handling" (also known as "rescuing" or "catching" an exception.

The basic syntax looks something like this:

#Imagine we write a function that would be called when an exception is raised.
def handle_exception
  puts "Got an exception, but I'm handling it!"
end

begin
  # Any exceptions in here...
rescue
  # ...will trigger this block of code
  handle_exception
end

# => "Got an exception, but I'm handling it!"

Enter fullscreen mode Exit fullscreen mode

In this example, an exception is raised, but the program does not crash because it was "rescued". Instead of crashing, Ruby runs the code in the rescue block, which prints out a message. This is cool and all, but all this does is tell us that something went wrong, without telling us what went wrong.

Any information about what went wrong is going to be contained within an exception object.

Exception Objects

Exception objects (E.g. SyntaxError or ArgumentError) are regular Ruby objects(subclasses of the Exception Class). They contain information about "what went wrong" for the exception that was rescued.

To get an exception object, the rescue syntax is slightly different. In this example, we'll rescue from a ZeroDivisionError exception class.

#Rescues all errors, and assigns the exception object to an `error` variable
rescue => error

#Rescues only ZeroDivisionError and assigns the exception object to an `error` variable
rescue ZeroDivisionError => error
Enter fullscreen mode Exit fullscreen mode

In the second example above, ZeroDivisionError is the class of the object in the error variable,ZeroDivisionError itself is a subclass of StandardError. The most standard error types in Ruby, such as ArgumentError and NameError are subclasses of StandardError. In fact, StandardError is the default exception for Ruby when an Exception subclass is not explicitly named.

begin
  do_something
rescue => error
  # This is the same as rescuing StandardError
  # and all of its subclasses
end
Enter fullscreen mode Exit fullscreen mode

Now.. let's go back and take a closer look at the exception object for ZeroDivisionError

begin
  # Any exceptions in here...
  1 / 0
rescue ZeroDivisionError => error
  puts "Exception Class: #{error.class}"
  puts "Exception Message: #{error.message}"
end

# Exception Class: ZeroDivisionError
# Exception Message: divided by 0
Enter fullscreen mode Exit fullscreen mode

Most Ruby exceptions contain a message detailing what went wrong.

Triggering Your Own Exceptions

You can also trigger your own exceptions! The process of raising your own exceptions is called.. well.. "raising". You do so by calling the raise method.

When raising your own exceptions, you choose which type of exception to use. You can also set your own error message.

begin
  #raises an ArgumentError with the message "You screwed up!"
  raise ArgumentError.new("You screwed up!")
rescue ArgumentError => error
  puts error.message
end

# => "You screwed up!"
Enter fullscreen mode Exit fullscreen mode

Creating Custom Exceptions

Ruby's built-in exceptions are great, but they are still limited in scope and cannot cover every possible use case.

Suppose you're building a web app and want to raise an exception when a user tries to access a part of the site that they shouldn't have access to? None of Ruby's built-in exceptions really fit, so your best bet is to create a custom exception.

To create a custom exception, simply create a new class that inherits from StandardError.

class PermissionDeniedError < StandardError

  def initialize(message)
    # Call the parent's(StandardError) constructor to set the message
    super(message)
  end

end

# Then, when the user tries to do something they don't
# have permission to do, you might do something like this:
raise PermissionDeniedError.new("Permission Denied!!!")
Enter fullscreen mode Exit fullscreen mode

The Exception Class Hierarchy

We made a custom exception by creating a subclass of StandardError, which itself is a subclass of Exception.
If you look at the class hierarchy, you'll see that everything subclasses Exception.

Don't worry, you don't need to memorize all of that. But it's important that you're familiar with class hierarchies in general for a very good reason.

Rescuing errors of a specific class will also rescue errors of its subclasses.

When you rescue StandardError, you not only rescue exceptions of StandardError but those of its children as well. If you look at the chart, you'll see that's a lot: ArgumentError, NameError, TypeError, etc.

Therefore, if you were to rescue Exception, you would rescue every single exception, which is a bad idea. Don't do this!

Ruby uses exceptions for things other than errors, so rescuing all errors will cause your program to break in weird ways. Always rescue specific exceptions. When in doubt, use StandardError.


Validation

Now that we have fair understanding of exceptions, let's look at validations and how it all ties in.

Many things that we do in our day to day lives can now be done online; whether it's shopping, banking, making dinner reservations, etc. But what's at the heart of all these processes? Data, it's all data.

Therefore, to ensure that everything runs smoothly, data integrity needs to be ensured. This is accomplished by validating the data that's provided before it can be saved to a database.

As you know, Rails is structured on the MVC architecture. The M, which is the model layer, is concerned with managing how objects are created and stored in a database. In Rails, this layer is run by Active Record by default, therefore, Active Record is concerned with handling the crucial task of data validation.

Validations are simply a set of rules that determine whether data is valid based on some criteria. Every model object contains an errors collection. In valid models, this collection contains no errors and is empty. When you declare validation rules on a certain model and it fails to pass, then an exception will be raised and that errors collection will contain errors consistent with the rules you've set, and this model will not be valid. A simple example is checking whether an online form has all the necessary data before it's submitted. If a field is left blank, an error message will be shown to the user so that they can fill it in.

Simple validations

Let's look at two basic validation methods.

Validating the presence and/or absence of attributes

This validation checks for the presence(or absence) of an attribute. In the example below, we're checking to see if the username attribute is not only present, but also unique. This helps you avoid having duplicate usernames within your application. You check these with the presence and uniqueness validation helpers respectively, using the validates keyword, followed by the attribute name:

class User < ApplicationRecord
   #validates presence and uniqueness of a username upon 
   #creation
   validates :username, presence: true, uniqueness: true
end
Enter fullscreen mode Exit fullscreen mode

Validating character length on an attribute

For a password attribute, we can check if the password that we're trying to set meets our specified requirements. In this case, we're making sure that it has a minimum length of 8 characters with the length helper:

class User < ApplicationRecord
  validates :username, presence: true, uniqueness: true
  #validates minimum length of 8 characters
  validates :password, length: { minimum: 8 }
end
Enter fullscreen mode Exit fullscreen mode

Working with Validation Errors

So how do you handle a validation error? As mentioned earlier, when a validation fails, an exception is raised and an errors collection is generated.

Let's see what happens when we try to create a user account with a password that is less than 8 characters:

class User < ApplicationRecord
  validates :username, presence: true, uniqueness: true
  validates :password, length: { minimum: 8 }
end

user = User.create(username: admin, password: "Welcome")

pp user

# => #<User:0x00007fd0cca748e8
# id: nil,
# username: "admin",
# password: "Welcome",
# created_at: nil,
# updated_at: nil> 
Enter fullscreen mode Exit fullscreen mode

We get back what appears to be a User instance, but upon closer inspection we see that the id attribute is nil, meaning the User instance wasn't saved to the database. Good to know, but we need to see why exactly this record is invalid. To raise an exception and see more details about the error, we need to append a bang(!) to our create method:

class User < ApplicationRecord
  validates :username, presence: true, uniqueness: true
  validates :password, length: { minimum: 8 }
end

user = User.create!(username: admin, password: "Welcome")

# => ActiveRecord::RecordInvalid (Validation failed: Password is # too short (minimum is 8 characters)) 
Enter fullscreen mode Exit fullscreen mode

Here we get a RecordInvalid exception, one of many exception subclasses of ActiveRecordError, which itself is a subclass of StandardError. See the list of Active Record exceptions here.

RecordInvalid is raised when a record cannot be saved to the database due to a failed validation. In this case, our password was too short.

Remember I mentioned that an errors collection is also generated? Here's how to access it:

class User < ApplicationRecord
  validates :username, presence: true, uniqueness: true
  validates :password, length: { minimum: 8 }
end

user = User.create!(username: nil, password: "Welcome")
# => ActiveRecord::RecordInvalid (Validation failed: Username can't be blank, Password is too short (minimum is 8 characters))

#Accesses an array of error messages from the `errors` object.
user.errors.full_messages
# => ["Username can't be blank", "Password is too short (minimum is 8 characters)"] 
Enter fullscreen mode Exit fullscreen mode

Having an easily accessible array of validation errors is useful because we can communicate them to the frontend.


Going back to our User model, let's add a create method to create a new user account.

def create
    #remember to append a "!" to your create method
    user = User.create!(user_params)
    session[:user_id] = user.id
    render json: user, status: :created
end

private

def user_params
    params.permit(:username, :password, 
    :password_confirmation)
end
Enter fullscreen mode Exit fullscreen mode

Now in the event of a failed validation, in this case a blank username and/or a short password, a RecordInvalid exception will be raised. You can now add some conditional logic to display errors back to the frontend, let's update our create method:

def create
  begin
    user = User.create!(user_params)
    session[:user_id] = user.id
    render json: user, status: :created
  rescue ActiveRecord::RecordInvalid => exception
    render json: {errors: 
    exception.record.errors.full_messages}, status: 
    :unprocessable_entity
  end
end
Enter fullscreen mode Exit fullscreen mode

In our rescue block, we are passing in the exception as an argument and accessing our errors collection located at exception.record.errors.full_messages. Then we wrap it up in a nice JSON format so that it may be passed to the frontend, with a HTTP status code of 422 aka Unprocessable Entity using the status: method. You could also just type status: 422, but this is more readable.

Isn't there an easier way?

You can see how this can get repetitive fast, especially if you're working with multiple models and controllers. You'd have to have the same begin/rescue block in all create actions throughout your controllers just to handle this one exception. Thankfully there's a way to abstract away some of this logic and make your life easier.

In Rails, you'll notice each controller inherits from ApplicationController, and thus inherits all of its methods. We can take advantage of this by moving the logic for handling a RecordInvalid exception up to ApplicationController, but we'll do it a little differently this time:

class ApplicationController < ActionController::API
   #rescue_from takes an exception class, and an exception 
   #handler method
   rescue_from ActiveRecord::RecordInvalid, with: 
   :unprocessable_entity_response

   private

   def unprocessable_entity_response(exception)
     render json: {errors: 
     exception.record.errors.full_messages}, status: 
     :unprocessable_entity
   end
end
Enter fullscreen mode Exit fullscreen mode

Instead of a begin/rescue block, we'll use the rescue_from method, which takes an exception class, and an exception handler specified by a trailing with: option containing the name of an exception handler method. In this case we created a private class method called unprocessable_entity_response. So, when the specified exception is raised, rescue_from will pass the exception object to the exception handler specified in the with: option.

On the frontend...

Displaying validation errors on the frontend is as simple as getting them through a fetch request and using conditional rendering to display them to a user when a validation error occurs.

This is an example from an account sign up form that I made previously using React:

function SignupForm() {
    const [username, setUsername] = useState("")
    const [password, setPassword] = useState("")
    const [passwordConfirmation, setPasswordConfirmation] = 
    useState("")

   //Any validation errors that are returned will be stored 
   in state...
    const [errors, setErrors] = useState([])

    async function handleSubmit(e) {
        e.preventDefault();

        const res = await fetch("/api/signup", {
            method: "POST",
            headers: {
                "Content-Type": "application/json"
            },
            body: JSON.stringify({
                username,
                password,
                password_confirmation: passwordConfirmation
            })
        })

        if (res.ok) {
            const user = await res.json()
            setUser(user)
        } else {
            //if the fetch request fails and returns an error 
            object, the errors will be stored in state using 
            setErrors
            const err = await res.json()
            setErrors(err.errors)
        }

    }
Enter fullscreen mode Exit fullscreen mode

In the return statement, you can use conditional rendering to display the errors if they are present in the errors array in state:

return (
   {errors.map((err) => <Alert key={err} severity="error">{err}</Alert>)}
)
Enter fullscreen mode Exit fullscreen mode

The result:


Resources

https://guides.rubyonrails.org/active_record_validations.html
http://www.railsstatuscodes.com/
https://www.rubydoc.info/docs/rails/4.1.7/ActiveRecord/ActiveRecordError
https://ruby-doc.org/core-2.5.1/Exception.html

Top comments (0)