Hi, my name is Rob, and I forgot to check for exceptions.

Methods that rely on third-parties that don't handle exceptions make for sad unicorns. Let Rob Tarr explain how to make your unicorns happy with better exception handling.

I was on my way to the office when I saw the messages that said that there was an error in one of our apps. It was failing in production. So, I ran into the office and immediately loaded it up to start exploring the error logs to see what was happening. The following is based upon actual events.

Calling to an external website

When this app updates certain fields in the database, it needs to call out to an external website to clear the cache so that on the next request, this external site will fetch new data. The domain name that it calls to is configured through the app by the users (remember this point).

So, when I first wrote this code, I threw together a quick method to make this call:

def clear_cache domain
domain.gsub!(/http\:\/\//,"")
Net::HTTP.get(domain, "/")
end
clear_cache "http://example.com"
view raw original-method.rb hosted with ❤ by GitHub

How not to handle exceptions

This worked great with my test input http://domain_name.com. But, if you remember (I told you to remember) this field is entered by the USER. And what the user had entered was http://domain_name.com/. It's a slight change, but that trailing slash made all the difference to my exception handling-less method. Methods that rely on third-parties that don't handle exceptions make for sad unicorns. It seems that the tests for this method were greatly lacking.

How to handle exceptions

There are a couple of things that I did to fix this:

First, I added some more test cases with varying degrees of accurate and inaccurate domain names. Then I made the regex more forgiving. Instead of /http\:\/\// (Try it out), I'm now using /(?:[^:]*:\/\/)?([^\/]+\.[^\/]+)/ (Try it out), which basically ignores anything before AND after the actual domain name. While doing just this would have fixed the problem that actually occurred, it wouldn't have really solved the problem.

def clear_cache domain
domain_parts = domain.match(/(?:[^:]*:\/\/)?([^\/]+\.[^\/]+)/)
domain = domain_parts[1]
Net::HTTP.get(domain, "/")
end
view raw better-regex.rb hosted with ❤ by GitHub

Second, I did some initial checks on the incoming parameter to make sure it was valid.

def clear_cache domain
raise "No domain specified." if domain.nil?
domain_parts = domain.match(/(?:[^:]*:\/\/)?([^\/]+\.[^\/]+)/)
domain = domain_parts[1]
Net::HTTP.get(domain, "/")
end
view raw check-for-nil.rb hosted with ❤ by GitHub

We then had a safeguard in place in case the method got called with a nil domain value.

Lastly—and this is a big one—I added a check around the third-party service. Lesson learned: don't get your tests passing and then move on. It's easy to forget about the exceptions that could come from a call to an external service: network timeout, resource moved, bad URI, etc. Any one of these could (and probably will) crash your app if it's not handled properly.

I also added a better implementation of the HTTP call, making sure that it accounts for SSL, if needed.

require 'logger'
require 'net/http'
@log = Logger.new(STDOUT)
@log.level = Logger::WARN
def clear_cache domain
if domain.nil?
@log.warn "No domain specified."
else
begin
uri = URI(domain)
Net::HTTP.start(uri.host, uri.port, :use_ssl => uri.scheme == 'https') do |http|
request = Net::HTTP::Get.new uri
end
rescue
@log.warn "Error reaching #{domain}."
end
end
end
clear_cache "robtarr.net"
puts "This code should still execute without any issues."

Now, the app handles exceptions in a pretty generic way, and it could catch different exceptions and respond to them in different ways. Currently it's simply swallowing the errors and moving on. For right now, I just want to be sure that any errors from this method get caught and don't halt the execution of the app.

There are other options to improve this reaction to unexpected outcomes. You could use something like rollbar that logs errors in a production site so that you can monitor the errors that your users are getting. Or maybe respond to the user to let them know there was an error, and give the user an opportunity to fix the problem. In our case, maybe we could tell them that the URL they entered did not work and give them the opportunity to change it and retry.

Wrap It Up

External services definitely need a lot of attention when it comes to handling the exceptions they serve up. Don't simply rely on passing tests to call it done. This was a simple example for a simple case. My buddy Don recommends these books for further reading on the subject: Exceptional Ruby by Advi Grim and Release It! by Richard Nygn.

I hope this helps you write safer code. Exceptions can be tricky: they’re paths in the code that are unwanted and often unexpected. It's always good to write some code to handle those that we know might happen. But it's also important to be able to at least fail gracefully when there are exceptions that we can't recover from.