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.
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
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
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
Now it’s clear what this does — and no existing code gets broken.
3. You’re Hiding Failures Instead of Fixing Them
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
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
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
Used like this:
begin
data = SunriseSunsetAPI.new(lat: 40.71, lng: -74.01).fetch
rescue => e
puts "Failed to retrieve data: #{e.message}"
end
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
Top comments (0)