Exceptions in Ruby

Exceptions in Ruby

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 useKernel#raisepassing 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 aRuntimeError:

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 samerescuedeclaration:

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

You can also add multiplerescuedeclarations:

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 therescueblocks is relevant: the first match is the one executed. Therefore, if you putStandardErroras the first condition and all your exceptions inherit fromStandardError, then the otherrescuestatements 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 likedef,class, andmodule. 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 extendsExceptionor a subclass ofException.

In general, you should always extendStandardErroror a descendant. TheExceptionfamily 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 theErrorsuffix at the end:

  • ConnectionError
  • DontPanicError

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

  • FileNotFoundvsFileNotFoundError
  • DatabaseExplodedvsDatabaseExplodedError

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