Ruby Exceptions

Subscribe to receive new articles. No spam. Quality content.

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:

Exceptions Hierarchy

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 throws RuntimeError
  • rescue by default catches only StandardError and all inherited exceptions
  • explicit return from ensure 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!

Subscribe to receive new articles. No spam. Quality content.

Comments