📣 This post originally appeared as Custom Exception Handling in Ruby on The Bearer Blog.
In Ruby, like in most languages, an exception is a way to convey that something went wrong. While some languages only use exceptions for truly exceptional circumstances, like run-time errors, Ruby uses exceptions for a wide variety of errors and unexpected results.
In this article, we will look at:
- How to handle common errors
- How to react to a specific exception
- How to create your own custom exceptions
How to raise and rescue exceptions in Ruby
Your own functions, Ruby, or gems and frameworks can raise exceptions internally. If you are familiar with Javascript of a C-inspired language, you may know of the try/catch
concept. Ruby has something similar for handling exceptions, using the begin-end
block. It can contain one or more rescue
clauses. Let's look at an example:
begin # Your code cause_an_error() rescue puts "An error has occurred" end
This is one way to build a basic ruby exception handler. This is also a way to catch common errors, as rescue
without any arguments will catch any StandardError
and its subclasses.
You can also assign the exception to a local variable, using rescue => var_name
, to gain access to properties of the exception. For example:
# ... rescue => error puts "#{error.class}: #{error.message}" end
We said that rescue
on its own catches StandardError
and its subclasses. You can also specify the error type you'd like to target. This allows you to use multiple rescue
clauses. For example:
begin # do something that raises an exception do_something() rescue NameError puts "A NameError occurred. Did you forget to define a variable?" rescue CustomError => error puts "A #{error.class} occurred: #{error.message}" end
In this example, the multiple rescue blocks handle each error type separately.
You can also rescue multiple error types at the same time by separating them with a comma. For example:
# ... rescue NameError, CustomError, ArgumentError => error # handle errors end
In these examples, we use begin
and end
. You can also use rescue
inside a method or block without the need for begin and end. For example:
def my_function perform_action() rescue => error puts error.message end
Else and ensure
In addition to begin-end
and rescue
, Ruby offers else
and ensure
. Let's look at an example:
begin # perform some actions rescue => error puts "Handle the #{error.class}" else # Code in the else block will run if no exception occurs do_someting_else() ensure # Code in the ensure block will always run always_do_this() end
Any code in the else
block will run if no exceptions occur. This is like a "success" block. Code in the ensure
block will run every time, even if the code does not throw an exception.
To summarize:
- The code in
begin
will try to run. - If an error occurs and throws an exception,
rescue
will handle the specified error types. - If no error occurs, the
else
block will run. - Finally,
ensure
's block will run always.
Raising specific exceptions
Now that we know how to handle, or rescue, errors and exceptions we can look at how to raise or throw an exception. Every exception your code rescues was raised at some location in the stack.
To raise a StandardError
:
raise StandardError.new('Message') # OR raise "Message"
Since StandardError
is the default exception type for raise
, you can omit creating a new instance and instead pass the message on its own.
Let's look at this in the context of a function and a begin-end
block.
def isTrue(x) if x puts "It's true" else raise StandardError.new("Not a truthy value") end end begin isTrue(false) rescue => error puts error.message end
Here we have an isTrue
function that takes an argument (x) and either puts
if x is true or raises an error if it is false. Then, in the begin block rescue
ensures that the code recovers from the raise
within isTrue
. This is how you will commonly interact with errors. By recovering from a raise
that occurred in another part of your codebase.
Creating custom exceptions
If you are building a gem or library, it can be useful to customize the types of errors your code raises. This allows consumers of your code to rescue based on the error type, just as we've done in the examples so far. To create your own exceptions, it is common to make them subclasses of StandardError
.
class MyCustomError < StandardError; end
Now, you can raise MyCustomError
when necessary, and your code's consumers can rescue MyCustomError
.
You can also add properties to custom exception types just as you would any other class. Let's look at an example of a set of errors for a circuit breaker. The circuit breaker pattern is useful for adding resiliency to API calls. For our purposes, all you need to know is that there are three states. Two of them may cause an error.
Let's create a custom error that, instead of just taking an error message, also takes the state
of our circuit.
class CircuitError < StandardError def initialize(message, state) super(message) @state = state end attr_reader :state end
The new CircuitError
class inherits from StandardError
, it passes message
to the parent class and makes state
accessible to the outside.
Now, if we look at this in the context of a rescue
, we can see how it might be used.
begin raise CircuitError.new("The circuit breaker is active", "OPEN") rescue CircuitError => error puts "#{error.class}: #{error.message}. The STATE is: #{error.state}." # => CircuitError: The circuit breaker is active. The STATE is: OPEN. end
The rescue block can now take advantage of the added state
property that exists on the error instance.
Adding custom exceptions to your modules
If you are developing a module, you can also take this a step further by incorporating custom error types into the module. This allows for better name-spacing and makes it easier to identify where the error is coming from. For example:
module MyCircuit # ... module Errors class CircuitError < StandardError; end end # ... end
You can raise the error type as follows:
raise MyCircuit::Errors::CircuitError.new('Custom error message')
It is common for libraries to include a set of subclassed error types. Generally, these live in exceptions.rb
or errors.rb
file within the library.
Rescue exceptions based on the parent class
So far we've seen how to rescue exceptions, raise exceptions, even create our own. One more trick is the ability to recover from errors based on their parent class.
Let us take our circuit breaker error from earlier and split it into one parent and two children. Rather than require the raise
clauses to pass in arguments, we will handle that in the errors themselves.
class CircuitError < StandardError def initialize(message) super(message) end end class CircuitOPEN < CircuitError def initialize super('The Circuit Breaker is OPEN') end end class CircuitHALF < CircuitBreaker def initialize super('The Circuit Breaker is HALF-OPEN') end end
Here, both CircuitOPEN
and CircuitHALF
are subclasses of CircuitError
. This may not seem useful, but it allows us to check for either error individually, or all subclasses of CircuitError
. Let's see what that looks like.
To rescue
them individually would look like:
begin # ... raise CircuitOPEN rescue CircuitHALF # do something on HALF error rescue CircuitOPEN # do something on OPEN error end
We could also group them together:
rescue CircuitHALF, CircuitOPEN # do something on both
But even better, since they are both subclasses of CircuitError
, we can rescue them all by rescuing CircuitError
.
begin raise CircuitHALF # or raise CircuitOPEN rescue CircuitError # will catch both HALF or OPEN end
By utilizing this method, your code can react to types of errors in addition to specific errors.
How to manage errors
This approach to handling exceptions in ruby goes a long way toward providing valuable errors to consumers of your code. Not only can you throw and raise exceptions, but your users can now catch exceptions in their ruby code that may come from your library.
At Bearer, we use an approach similar to this in our Agent codebase to provide our users with error details that help them identify where the problem is coming from.
If you're interested in preventing errors in your codebase, check out what we're building at Bearer. The Bearer Agent makes handling API inconsistencies easier by offering automatic remediations, resiliency patterns, and notifications with no changes to your code.
Explore more on the Bearer Blog and connect with us @BearerSH.
Top comments (0)