«Never rescue Exception in Ruby!»
Maybe you’ve heard this before. It’s good advice, but it’s pretty confusing unless you’re already in the know. Let’s break this statement down and see what it means.
You probably know that in Ruby, you can rescue exceptions like so:
begin
do_something()
rescue => e
puts e # e is an exception object containing info about the error.
end
And you can rescue specific errors by providing the classname of the error.
begin
do_something()
rescue ActiveRecord::RecordNotFound => e
puts e # Only rescues RecordNotFound exceptions, or classes that inherit from RecordNotFound
end
Every type of exception in Ruby is just a class. In the example above, ActiveRecord::RecordNotFound is just the name of a class that follows certain conventions.
This is important because when you rescue RecordNotFound
, you also rescue any exceptions that inherit from it.
Why you shouldn’t rescue Exception
The problem with rescuing Exception
is that it actually rescues every exception that inherits from Exception
. Which is….all of them!
That’s a problem because there are some exceptions that are used internally by Ruby. They don’t have anything to do with your app, and swallowing them will cause bad things to happen.
Here are a few of the big ones:
-
SignalException::Interrupt — If you rescue this, you can’t exit your app by hitting control-c.
-
ScriptError::SyntaxError — Swallowing syntax errors means that things like
puts("Forgot something)
will fail silently. -
NoMemoryError — Wanna know what happens when your program keeps running after it uses up all the RAM? Me neither.
begin
do_something()
rescue Exception => e
# Don't do this. This will swallow every single exception. Nothing gets past it.
end
I’m guessing that you don’t really want to swallow any of these system-level exceptions. You only want to catch all of your application level errors. The exceptions caused YOUR code.
Luckily, there’s an easy way to to this.
Rescue StandardError Instead
All of the exceptions that you should care about inherit from StandardError
. These are our old friends:
-
NoMethodError — raised when you try to invoke a method that doesn’t exist
-
TypeError — caused by things like
1 + ""
-
RuntimeError — who could forget good old RuntimeError?
To rescue errors like these, you’ll want to rescue StandardError
. You COULD do it by writing something like this:
begin
do_something()
rescue StandardError => e
# Only your app's exceptions are swallowed. Things like SyntaxErrror are left alone.
end
But Ruby has made it much easier for use.
When you don’t specify an exception class at all, ruby assumes you mean StandardError. So the code below is identical to the above code:
begin
do_something()
rescue => e
# This is the same as rescuing StandardError
end
Custom Exceptions Should Inherit from StandardError
So what does this mean for you if you’re creating your own custom exceptions?
It means you should always inherit from StandardError
, and NEVER from Exception
. Inheriting from Exception is bad because it breaks the expected behavior of rescue. People will think they’re rescuing all application-level errors but yours will just sail on through.
class SomethingBad < StandardError
end
raise SomethingBad
The Exception Tree
Since Ruby’s exceptions are implemented in a class heirarchy, it can be helpful to see it laid out. Below is a list of exception classes that ship with Ruby’s standard library. Third-party gems like rails will add additional exception classes to this chart, but they will all inherit from some class on this list.
Exception
NoMemoryError
ScriptError
LoadError
NotImplementedError
SyntaxError
SignalException
Interrupt
StandardError
ArgumentError
IOError
EOFError
IndexError
LocalJumpError
NameError
NoMethodError
RangeError
FloatDomainError
RegexpError
RuntimeError
SecurityError
SystemCallError
SystemStackError
ThreadError
TypeError
ZeroDivisionError
SystemExit
fatal
Built-in Exception Classes
NoMemoryError
Raised when memory allocation fails.
NoMemoryError Reference
ScriptError
ScriptError is the superclass for errors raised when a script can not be executed because of a LoadError
, NotImplementedError
or a SyntaxError
. Note these type of ScriptErrors
are not StandardError
and will not be rescued unless it is specified explicitly (or its ancestor Exception
).
ScriptError Reference
LoadError
Raised when a file required (a Ruby script, extension library, …) fails to load.
require 'this/file/does/not/exist'
raises the exception:
LoadError: no such file to load -- this/file/does/not/exist
LoadError Reference
NotImplementedError
Raised when a feature is not implemented on the current platform. For example, methods depending on the fsync
or fork
system calls may raise this exception if the underlying operating system or Ruby runtime does not support them.
Note that if fork
raises a NotImplementedError
, then respond_to?(:fork)
returns false
.
NotImplementedError Reference
SyntaxError
Raised when encountering Ruby code with an invalid syntax.
raises the exception:
SyntaxError: (eval):1: syntax error, unexpected '=', expecting $end
SyntaxError Reference
SecurityError
No longer used by internal code.
SecurityError Reference
SignalException
Raised when a signal is received.
begin
Process.kill('HUP',Process.pid)
sleep # wait for receiver to handle signal sent by Process.kill
rescue SignalException => e
puts "received Exception #{e}"
end
produces:
received Exception SIGHUP
SignalException Reference
Interrupt
Raised when the interrupt signal is received, typically because the user has pressed Control-C (on most posix platforms). As such, it is a subclass of SignalException
.
begin
puts "Press ctrl-C when you get bored"
loop {}
rescue Interrupt => e
puts "Note: You will typically use Signal.trap instead."
end
produces:
Press ctrl-C when you get bored
then waits until it is interrupted with Control-C and then prints:
Note: You will typically use Signal.trap instead.
Interrupt Reference
StandardError
The most standard error types are subclasses of StandardError. A rescue clause without an explicit Exception class will rescue all StandardErrors (and only those).
def foo
raise "Oups"
end
foo rescue "Hello" #=> "Hello"
On the other hand:
require 'does/not/exist' rescue "Hi"
raises the exception:
LoadError: no such file to load -- does/not/exist
StandardError Reference
ArgumentError
Raised when the arguments are wrong and there isn’t a more specific Exception class.
Ex: passing the wrong number of arguments
raises the exception:
ArgumentError: wrong number of arguments (given 2, expected 1)
Ex: passing an argument that is not acceptable:
raises the exception:
ArgumentError: negative array size
ArgumentError Reference
UncaughtThrowError
Raised when throw
is called with a tag which does not have corresponding catch
block.
raises the exception:
UncaughtThrowError: uncaught throw "foo"
UncaughtThrowError Reference
EncodingError
EncodingError is the base class for encoding errors.
EncodingError Reference
FiberError
Raised when an invalid operation is attempted on a Fiber, in particular when attempting to call/resume a dead fiber, attempting to yield from the root fiber, or calling a fiber across threads.
fiber = Fiber.new{}
fiber.resume #=> nil
fiber.resume #=> FiberError: dead fiber called
FiberError Reference
IOError
Raised when an IO operation fails.
File.open("/etc/hosts") {|f| f << "example"}
#=> IOError: not opened for writing
File.open("/etc/hosts") {|f| f.close; f.read }
#=> IOError: closed stream
Note that some IO failures raise `SystemCallError’s and these are not subclasses of IOError:
File.open("does/not/exist")
#=> Errno::ENOENT: No such file or directory - does/not/exist
IOError Reference
EOFError
Raised by some IO operations when reaching the end of file. Many IO methods exist in two forms,
one that returns nil
when the end of file is reached, the other raises EOFError
.
EOFError
is a subclass of IOError
.
file = File.open("/etc/hosts")
file.read
file.gets #=> nil
file.readline #=> EOFError: end of file reached
EOFError Reference
IndexError
Raised when the given index is invalid.
a = [:foo, :bar]
a.fetch(0) #=> :foo
a[4] #=> nil
a.fetch(4) #=> IndexError: index 4 outside of array bounds: -2...2
IndexError Reference
KeyError
Raised when the specified key is not found. It is a subclass of IndexError.
h = {"foo" => :bar}
h.fetch("foo") #=> :bar
h.fetch("baz") #=> KeyError: key not found: "baz"
KeyError Reference
StopIteration
Raised to stop the iteration, in particular by Enumerator#next
. It is rescued by Kernel#loop
.
loop do
puts "Hello"
raise StopIteration
puts "World"
end
puts "Done!"
produces:
StopIteration Reference
ClosedQueueError
The exception class which will be raised when pushing into a closed Queue. See Queue#close
and SizedQueue#close
.
ClosedQueueError Reference
LocalJumpError
Raised when Ruby can’t yield as requested.
A typical scenario is attempting to yield when no block is given:
def call_block
yield 42
end
call_block
raises the exception:
LocalJumpError: no block given (yield)
A more subtle example:
def get_me_a_return
Proc.new { return 42 }
end
get_me_a_return.call
raises the exception:
LocalJumpError: unexpected return
LocalJumpError Reference
NameError
Raised when a given name is invalid or undefined.
raises the exception:
NameError: undefined local variable or method `foo` for main:Object
Since constant names must start with a capital:
Integer.const_set :answer, 42
raises the exception:
NameError: wrong constant name answer
NameError Reference
NoMethodError
Raised when a method is called on a receiver which doesn’t have it defined and also fails to respond with method_missing
.
raises the exception:
NoMethodError: undefined method `to_ary` for "hello":String
NoMethodError Reference
RangeError
Raised when a given numerical value is out of range.
raises the exception:
RangeError: bignum too big to convert into `long`
RangeError Reference
FloatDomainError
Raised when attempting to convert special float values (in particular Infinity
or NaN
) to numerical classes which don’t support them.
Float::INFINITY.to_r #=> FloatDomainError: Infinity
FloatDomainError Reference
RegexpError
Raised when given an invalid regexp expression.
raises the exception:
RegexpError: target of repeat operator is not specified: /?/
RegexpError Reference
RuntimeError
A generic error class raised when an invalid operation is attempted. Kernel#raise
will raise a RuntimeError if no Exception class is specified.
raises the exception:
RuntimeError Reference
FrozenError
Raised when there is an attempt to modify a frozen object.
raises the exception:
FrozenError: can't modify frozen Array
FrozenError Reference
SystemCallError
SystemCallError is the base class for all low-level platform-dependent errors.
The errors available on the current platform are subclasses of SystemCallError and are defined in the Errno module.
File.open("does/not/exist")
raises the exception:
Errno::ENOENT: No such file or directory - does/not/exist
SystemCallError Reference
Errno
Ruby exception objects are subclasses of Exception. However, operating systems typically report errors using plain integers. Module Errno is created dynamically to map these operating system errors to Ruby classes, with each error number generating its own subclass of SystemCallError. As the subclass is created in module Errno, its name will start Errno::
.
The names of the Errno::
classes depend on the environment in which Ruby runs. On a typical Unix or Windows platform, there are Errno classes such as Errno::EACCES, Errno::EAGAIN, Errno::EINTR, and so on.
The integer operating system error number corresponding to a particular error is available as the class constant Errno::
error::Errno
.
Errno::EACCES::Errno #=> 13
Errno::EAGAIN::Errno #=> 11
Errno::EINTR::Errno #=> 4
The full list of operating system errors on your particular platform are available as the constants of Errno.
Errno.constants #=> :E2BIG, :EACCES, :EADDRINUSE, :EADDRNOTAVAIL, ...
Errno Reference
ThreadError
Raised when an invalid operation is attempted on a thread.
For example, when no other thread has been started:
This will raises the following exception:
ThreadError: stopping only thread
note: use sleep to stop forever
ThreadError Reference
TypeError
Raised when encountering an object that is not of the expected type.
raises the exception:
TypeError: no implicit conversion of String into Integer
TypeError Reference
ZeroDivisionError
Raised when attempting to divide an integer by 0.
42 / 0 #=> ZeroDivisionError: divided by 0
Note that only division by an exact 0 will raise the exception:
42 / 0.0 #=> Float::INFINITY
42 / -0.0 #=> -Float::INFINITY
0 / 0.0 #=> NaN
ZeroDivisionError Reference
SystemExit
Raised by exit
to initiate the termination of the script.
SystemExit Reference
SystemStackError
Raised in case of a stack overflow.
def me_myself_and_i
me_myself_and_i
end
me_myself_and_i
raises the exception:
SystemStackError: stack level too deep
SystemStackError Reference
fatal
fatal is an Exception that is raised when Ruby has encountered a fatal error and must exit.
fatal Reference
Handling exceptions in your API applications is quite an important thing, and if you want to keep things DRY, you should think how to do it in the proper way. In our Ruby on Rails API course, I’ve shown how to implement the error handling using ErrorSerializer and ActiveModelSerializers gem and here I’m going to show you even better approach to this topic when you can unify EVERY error across the whole API application.
Ruby On Rails REST API
The complete guide
Create professional API applications that you can hook anything into! Learn how to code like professionals using Test Driven Development!
Take this course!
UPDATE: I’ve recently came with even greater way of handling Errors in Rails Web applications using «dry-monads»! It still uses this approah to serilize the errors for JSON:API purposes, but the actual mapping can be done in the more neat way!
The final approach
There is no point to cover the whole thought process of how we came with the final result, but if you’re interested in any particular part just say it in the comments. The basic assumptions were to keep things DRY and unified across the whole application.
So here is the code.
The standard error.
# app/lib/errors/standard_error.rb
module Errors
class StandardError < ::StandardError
def initialize(title: nil, detail: nil, status: nil, source: {})
@title = title || "Something went wrong"
@detail = detail || "We encountered unexpected error, but our developers had been already notified about it"
@status = status || 500
@source = source.deep_stringify_keys
end
def to_h
{
status: status,
title: title,
detail: detail,
source: source
}
end
def serializable_hash
to_h
end
def to_s
to_h.to_s
end
attr_reader :title, :detail, :status, :source
end
end
First of all we needed to have the Base error, which will be a fallback for any exception risen by our application. As we use JSON API in every server’s response, we wanted to always return an error in the format that JSON API describes.
We extracted all error-specific parts for every HTML status code we wanted to support, having a fallback to 500.
More detailed errors
As you can see this basic error was just a scaffold we could use to override particular attributes of the error object. Having that implemented, we were able to instantiate several case-specific errors to deliver more descriptive messages to our clients.
# app/lib/errors/unauthorized.rb
module Errors
class Unauthorized < Errors::StandardError
def initialize
super(
title: "Unauthorized",
status: 401,
detail: message || "You need to login to authorize this request.",
source: { pointer: "/request/headers/authorization" }
)
end
end
end
# app/lib/errors/not_found.rb
module Errors
class NotFound < Errors::StandardError
def initialize
super(
title: "Record not Found",
status: 404,
detail: "We could not find the object you were looking for.",
source: { pointer: "/request/url/:id" }
)
end
end
end
All errors are very clean and small, without any unnecessary logic involved. That’s reasonable as we don’t want to give them an opportunity to fail in an unexpected way, right?
Anyway defining the error objects is only the half of the job.
Serializing the error in Ruby Application
This approach above allowed us to use something like:
...
def show
Article.find(params[:id])
rescue ActiveRecord::RecordNotFound
e = Errors::NotFound.new
render json: ErrorSerializer.new(e), status: e.status
end
...
To serialize the standard responses we use fast_jsonapi gem from Netflix. It’s quite nice for the usual approach, but for error handling not so much so we decided to write our own ErrorSerializer.
# app/serializers/error_serializer.rb
class ErrorSerializer
def initialize(error)
@error = error
end
def to_h
serializable_hash
end
def to_json(payload)
to_h.to_json
end
private
def serializable_hash
{
errors: Array.wrap(error.serializable_hash).flatten
}
end
attr_reader :error
end
The logic is simple. It accepts an object with status, title, detail and source methods, and creates the serialized responses in the format of:
# json response
{
"errors": [
{
"status": 401,
"title": "Unauthorized",
"detail": "You need to login to authorize this request.",
"source": {
"pointer": "/request/headers/authorization"
}
}
]
}
The only problem here is that handling all of those errors in every action of the system will end up with a lot of code duplications which is not very DRY, is it? I could just raise proper errors in the services, but standard errors, like ActiveRecord::RecordNotFound would be tricky. This is then what we ended up within our API ApplicationController:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
...
include Api::ErrorHandler
...
end
We just included the ErrorHandler module, where we implemented all mappings and the logic responsible for all error handling.
module Api::ErrorHandler
extend ActiveSupport::Concern
ERRORS = {
'ActiveRecord::RecordNotFound' => 'Errors::NotFound',
'Driggl::Authenticator::AuthorizationError' => 'Errors::Unauthorized',
'Pundit::NotAuthorizedError' => 'Errors::Forbidden'
}
included do
rescue_from(StandardError, with: lambda { |e| handle_error(e) })
end
private
def handle_error(e)
mapped = map_error(e)
# notify about unexpected_error unless mapped
mapped ||= Errors::StandardError.new
render_error(mapped)
end
def map_error(e)
error_klass = e.class.name
return e if ERRORS.values.include?(error_klass)
ERRORS[error_klass]&.constantize&.new
end
def render_error(error)
render json: Api::V1::ErrorSerializer.new([error]), status: error.status
end
end
At the top, we added a nice mapper for all errors we expect to happen somewhere. Then we rescue from the default error for the rescue block, which is the StandardError, and call the handle_error method with the risen object.
Inside of this method we just do the mapping of the risen error to what we have server responses prepared to. If none of them matches, we fall back to our Errors::StandardError object so client always gets the nice error message in the server response.
We can also add extra notifiers for any error that is not mapped in the handler module, so application admins will be able to track the unexpected results.
Rising errors in the application
In Driggl we managed to create a unified solution for the whole error handling across our API application. This way we can raise our errors in a clean way without repeating any rescue blocks, and our ApplicationController will always handle that properly.
def show
Article.find!(params[:id])
end
or
def authorize!
raise Errors::Unauthorized unless currentuser
end
Handling validation errors
Well, that is a nice solution, but there is one thing we intentionally omitted so far and it is: validation failure.
The problem with validations is that we can’t write the error object for invalid request just as we did for the rest, because:
- the failure message differs based on the object type and based on attributes that are invalid
- one JSON response can have multiple errors in the returned array.
This requires us add one more error, named Invalid, which is an extended version of what we had before.
# app/lib/errors/invalid.rb
module Errors
class Invalid < Errors::StandardError
def initialize(errors: {})
@errors = errors
@status = 422
@title = "Unprocessable Entity"
end
def serializable_hash
errors.reduce([]) do |r, (att, msg)|
r << {
status: status,
title: title,
detail: msg,
source: { pointer: "/data/attributes/#{att}" }
}
end
end
private
attr_reader :errors
end
end
You can see that the main difference here is the serialized_hash and initialize method. The initialize method allows us to pass error messages hash into our error object, so then we can properly serialize the error for every single attribute and corresponding message.
Our ErrorSerializer should handle that out of the box, returning:
# json response
{
"errors": [
{
"status": 422,
"title": "Unprocessable entity",
"detail": "Can't be blank",
"source": {
"pointer": "/data/attributes/title"
}
},
{
"status": 422,
"title": "Unprocessable entity",
"detail": "Can't be blank",
"source": {
"pointer": "/data/attributes/content"
}
}
]
}
The last thing, however, is to rise it somewhere, so the handler will get the exact error data to proceed.
In the architecture we have, it’s a not big deal. It would be annoying if we would go with updating and creating objects like this:
app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
def create
article = Article.new(article_params)
article.save!
end
private
def article_attributes
params.permit(:title)
end
end
As this would force us to rescue the ActiveRecord::RecordInvalid error in every action, and instantiate our custom error object there like this:
def create
article = Article.new(article_params)
article.save!
rescue ActiveRecord::RecordInvalid
raise Errors::Invalid.new(article.errors.to_h)
end
Which again would end up with repeating a lot of rescue blocks across the application.
In Driggl however, we do take advantage of Trailblazer architecture, with contracts and operations, which allows us to easily unify every controller action in the system.
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
def create
process_operation!(Admin::Article::Operation::Create)
end
def update
process_operation!(Admin::Article::Operation::Update)
end
end
I won’t go into details of Trailbalzer in this article, but the point is that we could handle the validation errors once inside of the process_operation! method definition and everything works like a charm across the whole app, keeping things still nice and DRY
# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
private
def process_operation!(klass)
result = klass.(serialized_params)
return render_success if result.success?
raise Errors::Invalid.new(result['contract.default'].errors.to_h)
end
def serialized_params
data = params[:data].merge(id: params[:id])
data.reverse_merge(id: data[:id])
end
def render_success
render json: serializer.new(result['model']), status: success_http_status
end
def success_http_status
return 201 if params[:action] == 'create'
return 204 if params[:action] == 'destroy'
return 200
end
end
Summary
You could think it’s a lot of code, but really, for big applications it’s just nothing comparing to repeating it in hundred of controllers and other files. In this form we managed to unify all our errors across the whole API application and we don’t need to worry anymore about unexpected failures delivering to the client.
I hope this will be useful for you too, and if you’ll find any improvements for this approach, don’t hesitate to let me know in the comments!
Special Thanks:
- Cristopher Jeschke for a nice cover image
Other resources:
- Custom exceptions in Ruby by Appsignal
- Error Hierarchy in Ruby by Starr Horne