topics #fedora #git #linux #ruby

Learn Ruby: 6. Exceptions, Errors, and Code Organization

Ruby includes several standard error types: RuntimeError, NoMethodError, NameError, ArgumentError, and others. The full list is in the official docs.

rescue

Use the begin → rescue → end structure to catch errors. The rescue => e form captures the error object into a variable:

class Calc
  def sum(a, b)
    begin
      result = a + b
      puts result
    rescue => e
      puts "Error: #{e.class} - #{e}"
    end
  end
end

calc = Calc.new

calc.sum("1", 2)
=> Error: TypeError - no implicit conversion of Integer into String

calc.sum(1, "23")
=> Error: TypeError - String can't be coerced into Integer

calc.sum(nil, nil)
=> Error: NoMethodError - undefined method `+' for nil:NilClass

If you don't need to declare variables before the block, you can omit begin — rescue then applies to the entire method body:

class Calc
  def sum(a, b)
    result = a + b
    puts result
  rescue => e
    puts "Error: #{e.class} - #{e}"
  end
end

calc = Calc.new

calc.sum({}, [])
=> Error: NoMethodError - undefined method `+' for {}:Hash

Catching every exception with bare rescue => e is often wrong — you should know exactly what you're handling. Pass the specific error class to rescue:

class Calc
  def sum(a, b)
    result = a + b
    puts result
  rescue TypeError
    puts "Our TypeError"
  rescue NoMethodError => e
    puts "Custom NoMethodError: #{e}"
  rescue => e
    puts "Error: #{e.class} - #{e}"
  end
end

calc = Calc.new

calc.sum(nil, nil)
=> Custom NoMethodError: undefined method `+' for nil:NilClass

calc.sum(1, "2")
=> Our TypeError

To see the exact line where the error occurred, call e.backtrace:

class Calc
  def div(a, b)
    result = a / b
    puts result
  rescue => e
    puts e.backtrace
  end
end

calc = Calc.new

calc.div(10, 0)
=>
calc.rb:3:in `/'
calc.rb:3:in `div'
calc.rb:12:in `<main>'

You can list multiple error types separated by a comma to handle them together:

class Calc
  def div(a, b)
    puts a / b
  rescue ZeroDivisionError, TypeError, NoMethodError
    puts "Something was wrong"
  rescue => e
    puts "Error: #{e.class} - #{e}"
  end
end

calc = Calc.new

calc.div(10, 0)
=> Something was wrong

calc.div("123", 1)
=> Something was wrong

raise

The raise keyword lets you throw any error, including a plain string message:

require 'json'

class Parser
  def parse(data)
    raise 'no_data' unless data

    JSON.parse(data)
  rescue JSON::ParserError
    puts "Wrong json"
  rescue => e
    puts "Other Error: #{e}"
  end
end

pr = Parser.new

pr.parse(nil)
=> Other Error: no_data

pr.parse("{a: 1}")
=> Wrong json

Custom Errors

Create your own error class by inheriting from StandardError:

class MyCustomError < StandardError
  def initialize(msg = 'Default error')
    super(msg)
  end
end

class Calc
  def sum(a, b)
    raise MyCustomError.new('My custom error here') if b < 0
    puts a + b
  rescue MyCustomError => e
    puts e
  end

  def sum2(a, b)
    raise MyCustomError.new if b < 0
    puts a + b
  rescue MyCustomError => e
    puts e
  end
end

calc = Calc.new

calc.sum(2, -3)
=> My custom error here

calc.sum2(2, -3)
=> Default error

retry

retry restarts the begin block from the top. Always use a counter to prevent an infinite loop:

class Sender
  def call
    retry_count = 0

    begin
      send_mail!
      puts 'sended'
    rescue => e
      if retry_count <= 3
        retry_count += 1
        puts 'retrying'
        retry
      end

      puts "Error: #{e}"
    end
  end

  def send_mail!
    puts "sending mail..."
    raise 'send error' if rand(10) < 5
  end
end

Sender.new.call

=>
sending mail...
sended

Sender.new.call

=>
sending mail...
retrying
sending mail...
retrying
sending mail...
retrying
sending mail...
sended

ensure

ensure runs its block regardless of whether an error was raised — ideal for cleanup and logging:

class Sender
  def call
    retry_count = 0

    begin
      send_mail!
      puts 'sended'
    rescue => e
      if retry_count <= 3
        retry_count += 1
        puts 'retrying'
        retry
      end

      puts "Error: #{e}"
    ensure
      puts "Log event"
    end
  end

  def send_mail!
    puts "sending mail..."
    raise 'send error' if rand(10) < 5
  end
end

Sender.new.call

=>
sending mail...
sended
Log event

Sender.new.call

=>
sending mail...
retrying
sending mail...
sended
Log event

Namespaces

Modules let you organize code when multiple classes share the same name. For example, a Pay class exists for both TBC and BOG banks — they live in separate folders:

tbc/pay.rb
bog/pay.rb
# tbc/pay.rb
module TBC
  URL = 'tbcbank.ge'

  class Pay
    def call
      puts "TBC pay - #{URL}"
    end
  end
end

# bog/pay.rb
module BOG
  URL = 'bog.ge'

  class Pay
    def call
      puts "BOG pay - #{URL}"
    end
  end
end

TBC::Pay.new.call
=> TBC pay - tbcbank.ge

BOG::Pay.new.call
=> BOG pay - bog.ge

Monkey Patching

In Ruby you can redefine any method by reopening an existing class:

name = 'Alex'

puts name.upcase
=> ALEX

class String
  def upcase
    self.split('').join('_')
  end
end

puts name.upcase
=> A_l_e_x

You can also extend existing types with entirely new methods:

puts 28.to_age
=> NoMethodError: undefined method `to_age' for 28:Integer

class Integer
  def to_age
    "Age: #{self}"
  end
end

puts 28.to_age
=> Age: 28

Monkey Patching causes serious maintenance problems: if a standard method is silently overridden somewhere in the codebase, the program can break in unexpected ways that are very hard to debug.

Refinements

Ruby 2.0 introduced Refinements as a safer alternative — they scope modifications to only the classes where you explicitly activate them. Declare a module and use refine to describe what you want to override:

module MyString
  refine String do
    def upcase
      self.split('').join('_')
    end
  end

  refine Integer do
    def to_building
      "Building: #{self}"
    end
  end
end

Activate the refinement in the desired class with using:

class CustomClassA
  using MyString

  def address(name, num)
    puts "Street: #{name.upcase}, #{num.to_building}"
  end
end

class CustomClassB
  def address(name, num)
    num = num.respond_to?(:to_building) ? num.to_building : num
    puts "Street: #{name.upcase}, #{num}"
  end
end

CustomClassA.new.address('Tsereteli', 123)
=> Street: T_s_e_r_e_t_e_l_i, Building: 123

CustomClassB.new.address('Tsereteli', 123)
=> Street: TSERETELI, 123

using also works inside a module:

module Money
  refine Integer do
    def to_d
      "$#{self}"
    end
  end
end

module BankMethods
  using Money

  def deposit_money(sum)
    puts "Your deposit: " + sum.to_d
  end
end

class Bank
  include BankMethods
end

Bank.new.deposit_money(100)
=> Your deposit: $100