DEV Community

Germán Alberto Gimenez Silva
Germán Alberto Gimenez Silva

Posted on • Originally published at rubystacknews.com on

The Pitfalls of Overusing rescue in Ruby (and How to Do It Right)

May 27, 2025

I’ve seen a lot of Ruby code over the years — some elegant, some messy, and some that made me pause and ask, “Why is this even working?”


🚀 Enhance Your Ruby App’s Error Handling!

Looking to improve error reporting and exception handling in your Ruby applications? Let’s connect and make your code cleaner, safer, and easier to maintain.

Get in Touch


One of those moments came when I saw something like this:


def safe_puts(value)
  puts value.inspect
rescue NoMethodError => e
  puts "Failed to print: #{e.message} (possibly undefined method on #{value.class})"
rescue TypeError => e
  puts "Type error while printing: #{e.message}"
rescue StandardError => e
  puts "Unexpected error while printing: #{e.class}: #{e.message}"
end

Enter fullscreen mode Exit fullscreen mode

At first glance, it looks clever — a method that wraps puts and catches any errors so nothing ever crashes. But as I dug deeper, I realized how dangerous this pattern can be.

So today, let’s talk about why this usage is problematic, what you should do instead, and how to write better error handling that actually helps you — not hides from you.


🧨 What’s Wrong With This Method?

Let’s break down the issues one by one.

1. You’re Rescuing Way Too Broadly

This method uses rescue => e, which means it catches every single exception — including serious ones like NoMemoryError, SystemStackError, or even Interrupt (which happens when someone hits Ctrl+C).

That’s not just overkill — it’s hiding real problems behind a veil of false safety.

Better approach: Rescue only the exceptions you expect:


def safe_print(value)
  puts value
rescue NoMethodError => e
  puts "Could not print: #{e.message}"
end

Enter fullscreen mode Exit fullscreen mode

Now, only relevant failures are caught. Everything else bubbles up, where it belongs.


2. You’re Stepping on Ruby’s Built-In Methods

By calling this method print, we’re overriding Ruby’s own Kernel#print. That might seem harmless at first, but it leads to confusion. Imagine debugging a script where print “hello” doesn’t behave the way you expect.

Better approach: Rename your method to avoid stepping on built-in behavior:


def safe_puts(value)
  puts value
rescue StandardError => e
  puts "Error: #{e.message}"
end

Enter fullscreen mode Exit fullscreen mode

Now it’s clear what this does — and no existing code gets broken.


3. You’re Hiding Failures Instead of Fixing Them

Article content
You’re Hiding Failures Instead of Fixing Them

Printing an error message may help during development, but in production, it’s just noise. Worse, it gives the illusion that everything is handled when, in fact, something has gone wrong.

Errors should be logged, monitored, and acted upon — not quietly dismissed.

Better approach: Use a logger instead of printing directly:


require 'logger'

LOGGER = Logger.new(STDOUT)

def debug_print(value)
  puts value.inspect
rescue StandardError => e
  LOGGER.error "Failed to print value: #{e.class} - #{e.message}"
end

Enter fullscreen mode Exit fullscreen mode

Now, you’re documenting the issue in a way that helps future maintainers — not just hiding it from view.


🛠 Real-World Example: Faraday API Calls

Let’s take a concrete example from the real world — using Faraday to fetch sunrise and sunset times from an external API.

Here’s a naive version:


begin
  conn = Faraday.new(url: "https://api.sunrisesunset.io ")
  response = conn.get('/json', lat: lat, lng: lng, date: date)
rescue => e
  puts "Something went wrong: #{e.message}"
end

Enter fullscreen mode Exit fullscreen mode

It looks safe — but again, it hides more than it reveals.

Let’s refactor this into something more maintainable, testable, and flexible.


require 'faraday'

class SunriseSunsetAPI
  def initialize(lat, lng, date = 'today')
    @lat = lat
    @lng = lng
    @date = date
  end

  def fetch
    response = connection.get('/json', lat: @lat, lng: @lng, date: @date)

    if response.success?
      return response.body
    else
      raise "API returned status #{response.status}: #{response.body}"
    end
  rescue Faraday::Error => e
    raise "Network error: #{e.message}"
  end

  private

  def connection
    @connection ||= Faraday.new(url: "https://api.sunrisesunset.io ")
  end
end

Enter fullscreen mode Exit fullscreen mode

Used like this:


begin
  data = SunriseSunsetAPI.new(lat: 40.71, lng: -74.01).fetch
rescue => e
  puts "Failed to retrieve data: #{e.message}"
end

Enter fullscreen mode Exit fullscreen mode

Why This Works Better

  • Reusable : You can use this class anywhere.
  • Testable : Easy to mock responses and verify behavior.
  • Flexible : Callers decide how to respond to failure.
  • Explicit : Errors bubble up clearly, so you know what went wrong.

🧭 Final Thoughts: Rescue Should Reveal, Not Conceal

Exception handling isn’t about avoiding failure — it’s about responding to it with clarity and control.

When used well, rescue gives you the power to manage uncertainty gracefully. When used poorly, it turns your code into a minefield of silent bugs and hidden surprises.

So next time you reach for rescue, ask yourself:

  • Am I rescuing only what I expect?
  • Am I giving the caller enough context to understand what went wrong?
  • Is my code clearer because of this, or more confusing?

Because in the long run, the best code doesn’t just work — it tells a story. And the best error handling makes that story honest, helpful, and easy to follow.


Have thoughts on exception handling patterns or want to share your own rescue horror stories? Drop a comment below 👇

Article content

Ruby #Programming #CodeQuality #BestPractices #CleanCode #SoftwareEngineering #Faraday #ErrorHandling #Rescue

Top comments (0)