Exceptions in Ruby
In Ruby, Exceptions are created using raise
command:
If we execute that code then we will see the error:
Notice that Ruby is saying that it is a RuntimeError
. Here is Ruby's official
documentation about
RuntimeError. Above code
can also be written like this:
As per Ruby's documentation if when we do not mention any class while raising an
exception then by default it is RuntimeError
class.
In Ruby all exceptions are subclasses of Exception
class.
Hierarchy of Ruby Exception class
Ruby has lots of built in exceptions. Here is hierarchy of all Ruby's
exceptions:
The rescue method catches a class and all its subclasses
Here we are rescuing all NameError
exceptions. However NoMethodError
will
also be rescued because NoMethodError
is a subclass of NameError
.
For example, consider a api_exceptions.rb
file where we are rescuing from all
the exceptions like this:
The RecordNotUnique
exception is a child class of StandardError
. When we
rescue from StandardError
all the child exception classes will also be caught.
So the rescue_from ActiveRecord::RecordNotUnique
in the above example is
redundant. The code will never reach this line because the RecordNotUnique
exception will already be caught by the rescue_from StandardError
statement.
The correct way of rescuing the errors will be, like this:
In the above example, we are rescuing StandardError
and all its children
classes with handle_api_exception
method. And inside handle_api_exception
method case is used to handle
the required child exception classes specifically.
Certain exceptions and when they are raised
-
Pundit::NotAuthorizedError
is raised when any authorization error from
Pundit gem raises and we
can handle it with 403
or forbidden
status.
-
ActionController::ParameterMissing
is raised when a required parameter is
missing. Consider a case when we have strong params
defined for a post
request and those params are missing in the request header. We can handle this
via 500
or internal_server_error
status.
-
ActiveRecord::RecordNotFound
is raised when Rails
does not find any
record. When we use find
method to search for a record with the provided
id
in params and the id
is mistaken or the record is missing from DB
,
Rails
raises ActiveRecord::RecordNotFound
exception. We can handle this
with a 404
or not_found
status.
-
ActiveRecord::RecordInvalid
is raised on the failure of validations declared in
model for any record creation or update actions. Let's say we have some email
validation declared in model and the input email does not match with the given
Regex
pattern Rails
raises ActiveRecord::RecordInvalid
exception. We can
handle this with a 422
or unprocessable_entity
status.
-
ActiveRecord::RecordNotUnique
is raised when a record cannot be inserted or
updated because it would violate a uniqueness constraint from DB
. We can
handle this with unprocessable_entity
or 422
status.
-
PG::NotNullViolation: ERROR: null value in column "name" of relation "users" violates not-null constraint
for missing mandatory input. We should handle the database errors starting
with PG::
or SQLite3::
with internal_server_error
or 500
status. If we
keep this as unprocessable_entity
then it won't raise Honeybadger issues,
meaning it will log the error silently. These errors are very rare and we
should be getting notified of these errors by Honeybadger in Github.
Raising error using class
Following two lines do the same thing:
We can raise exceptions of a particular class by stating the name of that
exception class:
Default rescue is StandardError
rescue
without any argument is same as rescuing StandardError
:
Above statement is same as the one given below:
Catching multiple types of exceptions in one shot
We can catch multiple types of exceptions in one statement:
Catching exception in a variable
We can catch exception in a variable like this:
Here e
is an exception object. The three main things we like to get from an
exception object are "class name", "message" and "backtrace".
Let's print all the three values:
Custom exceptions
Sometimes we need custom exceptions. Creating custom exceptions is easy:
NotAuthorizedError
is a regular Ruby class. We can add more attributes to it
if we want:
rescue nil
Sometimes we see code like this:
The above code is equivalent to the following code:
The above code can also be written like this, since by default StandardError
is
raised:
Exception handling in Ruby on Rails using rescue_from
A typical controller could look like this:
We can use
rescue_from
to catch the exception.
The rescue_from
directive is an exception handler that rescues the specified
exceptions raised within controller actions and reacts to those exceptions with
a defined method.
For example, the following controller rescues ActiveRecord::RecordNotFound
exceptions and passes them to the render_404
method:
The advantage to rescue_from
is that it abstracts the exception handling away
from individual controller actions, and instead makes exception handling a
requirement of the controller.
The rescue_from
directive not only makes exception handling within controllers
more readable, but also more regimented.
Rescuing from specific exception
Ruby’s Exception
is the parent class to all errors. So one might be tempted to
always rescue from this exception class and get the "job" done. But DON'T!
Exception
includes the class of errors that can occur outside your
application. Things like memory errors, or SignalException::Interrupt
(sent
when you manually quit your application by hitting Control-C
), etc. These are
the errors that you don’t want to catch in your application as they are
generally serious and related to external factors. Rescuing the Exception
class can cause very unexpected behaviour.
StandardError
is the parent of most Ruby and Rails errors. If you catch
StandardError
you’re not introducing the problems of rescuing Exception
, but
it is not a great idea either. Rescuing all application-level errors might cover
up unrelated bugs that you don’t know about.
The safest approach is to rescue the error(or errors) you are expecting and deal
with the consequences of that error inside the rescue block.
In the event of an unexpected error in your application you want to know that a
new error has occurred and deal with the consequences of that new error inside
its own rescue block.
Being specific with rescue means your code doesn’t accidentally swallow new
errors. You avoid subtle hidden errors that lead to unexpected behaviour for
your users and bug hunting for you.
Do not use exception as control flow
Let's look at the following code:
In the above code when quiz id is not found then an exception is raised and then
that exception is immediately caught.
Here the code is using exception as a control flow mechanism. What it means is
that the code is aware that such an exception could be raised and is prepared to
deal with it.
The another way to deal with such a situation would be to not raise the
exception in the first place. Here is an alternative version where code will not
be raising any exception:
In the above case instead of using find
code is using find_by_id
which would
not raise an exception in case the quiz id is not found.
In Ruby world we like to say that an exception should be an exceptional
case. Exceptional case could be database is down or there is some network
error. Exception can happen anytime but in this case code is not using catching
an exception as a control flow.
Long time ago in the software engineering world GOTO
was used a lot. Later
Edsger W. Dijkstra wrote a
famous letter
Go To Statement Considered Harmful.
Today it is a well established that using GOTO
is indeed harmful.
Many consider using Exception as a control flow similar to using GOTO since when
an exception is raised it breaks all design pattern and exception starts flowing
through the stack. The first one to capture the exception gets the control of
the software. This is very close to how GOTO works. In Ruby world it is well
established practice to
not to use Exception as a control flow.
Using bang methods in controller actions
But, just like everything in software engineering, the suggestion we had made in
last section also has some exceptions. Like in controllers etc, we should be
trying our level best to keep controllers as skinny as possible. So adding
repetitive unless
or if
statements as a replacement for rescue
statements
won't scale. Thus in such cases, what we should do is use the bang(!)
versions
of ActiveRecord methods, like create!
, update!
, destroy!
or save!
within
the controller actions. This would raise an exception in case there's a failure.
But where to handle theses exceptions? Well, in the chapter where we had cleaned
up the application controller, we had added a concern named ApiExceptions
.
This concern is included in the ApplicationController
. Which means all other
controllers will be having access to the methods defined in that concern. In
that concern we have several rescue_from
statements, which handles specific
exceptions.
Thus in our controller, we could write something like this, and both the success
and failure cases will be handled:
Notice how we have named the method as load_quiz!
with a bang(!)
rather than
simply load_quiz
? It's to denote that the particular method has the potential
to raise an exception. If no record is found by the find_by!
method then a
ActiveRecord::RecordNotFound
exception will be raised.
There is a problem in the above code block, which has to deal with code
conventions that we follow in BigBinary. That's, if we are querying using the
id
attribute only then we should use the find
method over find_by!
because
in the find
method we can directly pass the id
value without defining any
key and it makes the code cleaner. If for a given id
no record is found by
find
method then the same exception will be raised that is
ActiveRecord::RecordNotFound
.
So, in the above code block the load_quiz!
method needs to be updated like this:
There is nothing to commit in this chapter since all we had done was
learning the basics of exception handling in Ruby.