What are exceptions & Why They Happen?

We use exceptions to handle unexpected failures inside a program. Exceptions are in the form of a program crash with an error message.

In Ruby, Exceptions are implemented as classes inherited from the Exception class used to signal an error condition when the program cannot finish its work in the current state.

You might have seen exceptions a few times before, an example related to ActiveRecord

ActiveRecord::RecordNotFound- Couldn't find Article with 'id'=12
        activerecord (4.2.0) lib/active_record/core.rb:154:in 'find'
        app/controllers/articles_controller.rb:77:in 'set_article'
        activesupport (4.2.0) lib/active_support/callbacks.rb:427:in

This error message is composed of:

  • the exception class (ActiveRecord::RecordNotFound)
  • the exception message ("Couldn't find Article with id= 12)
  • the stack trace

These are important pieces of information to help us understand and fix the problem.

An exception does not always mean that your program has to end. For Example:

File.open("random_file.txt")

Assuming that random_file.txt doesn't exist, we will get an error:

open-file.rb:1:in 'initialize': File not found No such file or directory @ rb_sysopen - random_file.txt (Errno::ENOENT)

The exception, in this case, is Errno::ENOENT The Errno family of exceptions are all related to the operating system. We may not want this code to crash in the larger program, so we use the begin/rescue block. In other programming languages, there will be a try/catch/throw the block for exception handling For Example:

begin
    File.open("random_file.txt")
rescue Errno::ENOENT => e
    puts "File not found"
    puts e.message
    puts e.backtrace
end

This code will print the "File not found" with the Ruby Exception of Errno::ENOENT If we use rescue without an exception name then StandardError is the default trapped exception. This will also trap all the exceptions that inherit from StandardError.

Example without begin

def open_file
    File.open("random_file.txt")
rescue Errno::ENOENT => e
    puts "File not found"
    puts e.message
    puts e.backtrace
end

Another keyword related to exceptions that we can use is ensured.

The code ensured will always run, even if an exception happens.

For Example:

begin
    puts "test example"
ensure
    puts "Always print this test example"
end

Another example:

def call
    @article.lock
    begin
        puts "do something"
    ensure
        @article.unlock
    end
end

There is one more keyword: retry. When we use retry inside a rescue block, it will re-run the beginning block. I would recommend that we avoid using retry because we can easily get into infinite loops and other issues.

Exceptions and Return Values

The last thing evaluated in a method is what gets returned from the method which is also called implicit return The rescue part of our code doesn't get evaluated unless there is an exception. For Example:

def example_exception(exception = false)
    raise if exception
        1
    rescue
        "print no exception"
    end
end

p example_exception
# => 1
p example_exception(true)
# => "print no exception"

When a ruby program ends for any reason or if we need to end the program immediately without going through any kind of callbacks we can also use at_exit or exit! method This can be useful to write some crash logger.

at_exit do
    puts "Program finished at #{Time.now}"
end

We can also create our own custom exception classes, as Ruby has several built-in exception classes.

Example

class InvalidArticle < StandardError
end

raise InvalidArticle, "the article posted is invalid"

There are a list of built-in exceptions, you can check it out on this link