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
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!"
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
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
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
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!"
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!!!")
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
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
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>
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))
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)"]
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
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
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
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)
}
}
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>)}
)
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)