Exceptions in Ruby

Handling an exception

An exception is an event that disrupts the normal flow of the program. Ruby helps us to handle these events in a way that is suitable to our needs.

We can handle these events by declaring the code in between begin/rescue block to catch an exception.

Ways of handling exceptions

1.The General Way

    begin
        # an execution that may fail
    rescue
        # something to execute in case of failure
    End

Here the code in rescue block is executed by default whenever an exception is raised in the begin block

2.Checking for Exceptions

    begin
        # an execution that may fail
    rescue StandardError
        # Executes only when the begin block code raises a standardError
    end

In this case the rescue block is executed only when a standardError is raised. All the other exceptions go unhandled

3.Storing exception information

begin
        # an execution that may fail
    rescue StandardError => error
        # Executes only when the begin block code raises a standardError
puts error.message #Prints the error message.
    end
    
    In this case, after catching the exception, all the data related to the exception is written into the error object.


4.The retry command
    
begin
   #here goes your code
rescue StandardError => e
   #if you want to retry the code
   retry
end

When the compiler encounters the retry command, ruby automatically re-executes the code block present in begin.


5.The ensure command

begin
   #here goes your code
rescue StandardError => e
   puts e.message
ensure
   #This block is executed regardless of the way the exception is handled
   puts “This is from ensure block!” 
end

    The code written in the ensure block is always executed regardless of the way the exception is handled.

6.The raise statement
    We can raise an exception using the raise statement.
def hello(subject)
  raise ArgumentError, "`subject` is missing" if subject.to_s.empty?
  puts "Hello #{subject}"
end

hello # => ArgumentError: `subject` is missing
hello("Simone") # => "Hello Simone”

When the interpreter encounters the raise statement it throws an exception

Handling multiple exceptions
    We can handle multiple exceptions using two ways
Declaring multiple exceptions in a single rescue statement.    

begin
  # an execution that may fail
  rescue StandardError, ArgumentError, Exception
   # Executes when the begin block code raises a StandardError and Argument Error
End

7.Declaring multiple rescue statements

begin
  # an execution that may fail
  rescue StandardError, ArgumentError
    # Executes only when the begin block code raises a standardError
  rescue ArgumentError
    # Executes only when the begin block code raises an ArgumentError
End

    
8.Raising a Custom Exception
    Any class that extends an Exception or a subclass of an exception is said to be a custom exception.

Here is an example demonstrating the way in which we can declare a custom exception.
# Defines a new custom exception called FileNotFound
  class FileNotFound < StandardError
  end

  def read_file(path)
    File.exist?(path) || raise(FileNotFound, "File #{path} not found")
    File.read(path)
  end

  read_file("missing.txt")  #=> raises FileNotFound.new("File `missing.txt` not  found")
  read_file("valid.txt")    #=> reads and returns the content of the file    


 

Raising an exception

To raise an exception use Kernel#raise passing the exception class and/or message:

raise StandardError # raises a StandardError.new
raise StandardError, "An error" # raises a StandardError.new("An error")

You can also simply pass an error message. In this case, the message is wrapped into a RuntimeError:

raise "An error" # raises a RuntimeError.new("An error")

Here's an example:

def hello(subject)
  raise ArgumentError, "`subject` is missing" if subject.to_s.empty?
  puts "Hello #{subject}"
end

hello # => ArgumentError: `subject` is missing
hello("Simone") # => "Hello Simone"

Handling multiple exceptions

You can handle multiple errors in the same rescue declaration:

begin
  # an execution that may fail
rescue FirstError, SecondError => e
  # do something if a FirstError or SecondError occurs
end

You can also add multiple rescue declarations:

begin
  # an execution that may fail
rescue FirstError => e
  # do something if a FirstError occurs
rescue SecondError => e
  # do something if a SecondError occurs
rescue => e
  # do something if a StandardError occurs
end

The order of the rescue blocks is relevant: the first match is the one executed. Therefore, if you put StandardError as the first condition and all your exceptions inherit from StandardError, then the other rescue statements will never be executed.

begin
  # an execution that may fail
rescue => e
  # this will swallow all the errors
rescue FirstError => e
  # do something if a FirstError occurs
rescue SecondError => e
  # do something if a SecondError occurs
end

Some blocks have implicit exception handling like def, class, and module. These blocks allow you to skip the begin statement.

def foo
    ...
rescue CustomError
    ...
ensure
    ...
end

Creating a custom exception type

A custom exception is any class that extends Exception or a subclass of Exception.

In general, you should always extend StandardError or a descendant. The Exception family are usually for virtual-machine or system errors, rescuing them can prevent a forced interruption from working as expected.

# Defines a new custom exception called FileNotFound
class FileNotFound < StandardError
end

def read_file(path)
  File.exist?(path) || raise(FileNotFound, "File #{path} not found")
  File.read(path)
end

read_file("missing.txt")  #=> raises FileNotFound.new("File `missing.txt` not found")
read_file("valid.txt")    #=> reads and returns the content of the file

It's common to name exceptions by adding the Error suffix at the end:

  • ConnectionError
  • DontPanicError

However, when the error is self-explanatory, you don't need to add the Error suffix because would be redundant:

  • FileNotFound vs FileNotFoundError
  • DatabaseExploded vs DatabaseExplodedError

Adding information to (custom) exceptions

It may be helpful to include additional information with an exception, e.g. for logging purposes or to allow conditional handling when the exception is caught:

class CustomError < StandardError
  attr_reader :safe_to_retry

  def initialize(safe_to_retry = false, message = 'Something went wrong')
    @safe_to_retry = safe_to_retry
    super(message)
  end
end

Raising the exception:

raise CustomError.new(true)

Catching the exception and accessing the additional information provided:

begin
  # do stuff
rescue CustomError => e
  retry if e.safe_to_retry
end

Try

Try