Ruby Exceptions
- 07 March 2017
- Ruby
- Ruby Exceptions
Let's take a break from long reads and discuss something that Ruby-developers use every day. We're going to discuss exceptions. At first sight exceptions in Ruby look as relatively simple concept. But there are couple caveats.
If you don't know how exceptions work in Ruby check this example:
puts a # => undefined local variable or method `a' for main:Object (NameError)
a
is an undefined variable and that's why we get this exception with descriptive message: "undefined local variable or method a". Also we see a type of exception: NameError
.
If we know that our code can throw an exception, we can catch it using begin...rescue
.
begin
puts a
rescue
puts 'Something bad happened'
end
Also, we can define in rescue
which exact type of error we want to catch:
begin
puts a
rescue NameError => e
puts e.message
end
To catch different types of exceptions by rescue
we can use something like that:
def foo
begin
# logic
rescue NoMemoryError, StandardError => e
# process the error
end
end
It's really important to know that by default rescue
catches all errors which are inherited from StandardError
. So it's not going to catch NotImplementedError
or NoMemoryError
for example. To understand which exceptions we can catch by rescue
let's check hierarchy of exceptions in Ruby:
As you've noticed in rescue
we can define variable by which we can get an access to exception object:
def foo
begin
raise 'here'
rescue => e
e.backtrace # ["test.rb:3:in `foo'", "test.rb:10:in `<main>'"]
e.message # 'here'
end
end
You can get more info about methods of this object here.
Ruby provides another interesting keyword to work with exceptions - ensure
. Either exception appears or not - Ruby will execute code inside ensure
anyway. Very often developers use this to close connection to db, remove temporary files, etc.
begin
puts a
rescue NameError => e
puts e.message
ensure
# clean up the system, close db connection, remove tmp file, etc
end
There is one important thing to know about ensure
. If you explicitly return
from ensure
but don't not define rescue
- ensure
will intercept an exception.
Let's check it by example. We will start from implicit return
:
def foo
begin
raise 'here'
ensure
puts 'processed'
end
end
foo
# processed
# => `foo': here (RuntimeError)
In this example Ruby executes code inside ensure
first and then throws an exception because we didn't catch it by rescue
.
Let's consider example with explicit return
:
def foo
begin
raise 'here'
ensure
return 'processed'
end
end
puts foo # => processed
No RuntimeError
this time! We still don't catch an exception, but Ruby returns 'processed' from ensure
and don't throw that error.
Let's learn how to throw exceptions from code. Module Kernel
has method raise
which allows to throw errors. There is an alias for raise
- fail
, but you will see raise
much more often than fail
.
If you call raise
without params it will throw this error:
raise # => `<main>': unhandled exception
This exception doesn't say anything to developer, so usually you want to pass at least error message:
raise 'Could not read from database' # => Could not read from database (RuntimeError)
Now we see that raise
returned error message and it's a RuntimeError
. By default raise
throws RuntimeError
.
We can define which exact exception we want to throw:
raise NotImplementedError, 'Method not implemented yet'
# => Method not implemented yet (NotImplementedError)
It's interesting that raise
calls method #exception
for any class you pass to it. In this case it called NotImplementedError#exception
. It allows us to add exception support to any class. The main requirement is #exception
method:
class Response
# ...
def exception(message = 'HTTP Error')
RuntimeError.new(message)
end
end
response = Response.new
raise response # => HTTP Error (RuntimeError)
There is one more interesting thing about exceptions. When exception appears Ruby stores it in global variable $!
.
$! # => nil
begin
raise 'Exception'
rescue
$! # <RuntimeError: Exception>
end
$! # => nil
As we see here we have exception assigned to $!
inside rescue
part, but right after execution it equals to nil
.
Ruby provides us a way to run code inside begin
part one more time.
Let's imagine that we have service which doesn't return required data sometimes.
We could wrap requests to that service into loop, but we can use retry
, which will execute code inside begin
one more time.
tries = 0
begin
tries += 1
puts "Trying #{tries}..."
raise 'Did not work'
rescue
retry if tries < 3
puts 'I give up'
end
# Trying 1...
# Trying 2...
# Trying 3...
# I give up
This code is really simple. If code inside begin
throws an exception we try to execute it one more time. This idea is really interesting. But I should notice that for better error handling of third-party services there is a better solution called Circuit Braker: article, gem.
Also I would like to mention that if there was an exception and program finishes execution - Ruby will execute code inside callback at_exit
:
at_exit { puts 'going to exit' }
raise 'exception'
# => going to exit
# => exception (RuntimeError)
Ruby will execute it even if program is exiting with an exception.
The last interesting approach that Ruby-developers use - they use rescue
on method level without explicit definition of begin
.
def set_post
@post = Post.find(params[:id])
rescue ActiveRecord::RecordNotFound
redirect_to posts_path, alert: "Post Not Found"
end
It's an example of Rails-application which wants to find Post by id
. If it doesn't find it will throw an exception ActiveRecord::RecordNotFound
which we can catch by rescue
and redirect user to posts_path
.
Ruby it's a really laconic language, so it allows us to do shorter version:
def foo
# ...
rescue
# ...
end
It's much better than this:
def foo
begin
# ...
rescue
# ...
end
end
Long story short
Things to remember:
raise
by default throwsRuntimeError
rescue
by default catches onlyStandardError
and all inherited exceptions- explicit
return
fromensure
without error handling will intercept that exception - during error handling Ruby stores exception in global variable
$!
- Ruby has
retry
at_exit
will be executed even if program exits because of excecption- use
rescue
by method-level to make code shorter
I hope you've found some interesting ideas in this article. Send me a message if you also know something interesting about Exceptions in Ruby.
I've wrote this post after watching video by Avdi Grimm. You'll find couple more ideas there.
Thanks for reading!