Skip to main content

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

09-24-13 Rob Tarr

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"

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

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

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.

Related Content

Want to talk about how we can work together?

Katie can help

A portrait of Vice President of Business Development, Katie Jennings.

Katie Jennings

Vice President of Business Development