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.
#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.
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 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 (!) 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 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 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:
https://ruby-doc.org/core-2.5.1/Exception.html
SOCIAL SHARE CARD GENERATOR