Skip to content

Instantly share code, notes, and snippets.

@kevinclark
Created November 14, 2011 23:13
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kevinclark/1365529 to your computer and use it in GitHub Desktop.
Save kevinclark/1365529 to your computer and use it in GitHub Desktop.

On Failure and Flow

It was so simple. All you wanted was a local copy of your browser history so you could read offline. So you hacked up a script and it worked pretty well. But there were gaps. Sometimes pages didn't work by the time you tried to download them. Sometimes the server errored out. Once, the jerk next door dug up a cable line and your net connection went down entirely.

Errors are going to happen, so how do you handle them in code?

Well, C style return codes are simple. Just have your function return true or false to indicate success.

foo = bar(input)
if (! foo) return "invalid data"

But what if it's more complicated than that? Well, you could change the return code to an integer or some sort of enum.

But now there's ambiguity in whether the return code is indicative of success or failure, or if it's the value that's being returned. This can be settled with out variables, or documentation, but there's a discipline problem.

The problem is that using return codes requires discipline.

Wouldn't it be nice if the computer did that for you? Well, there's always the Checked Exception, a la Java.

Just declare what errors you're going to throw, whenever you're going to throw them, and the compiler will enforce that you're handling errors. It's as simple as:

try {
  myfun();
  // Keep going if it worked
} catch (SomeError e) {
  // Handle e
}

But this option takes up a lot of screen real estate. All the try/catch blocks obscure what the point of the code is. And if you have to wrap one more function call with a catch for InterruptedException, you might have strong words with the coder who suggested checked exceptions would make things easier. Safer, maybe, but now you pay the price for compiler safety at every turn.

So what's another option?

Some languages provide structures that give the simplicitly of the return code with the compiler assistance of the checked exception. In Scala (and Haskell), it's Either. Before we dig in, note that while I'm going to be giving examples in Scala, this style can and does exist in other languages. See Confident Code for similar ideas in Ruby.

An instance of Either represents one of two alternatives - typically a failure or a success. Because Either doesn't have to represent a failure or a success, what is usually the failure type is called Left and what is usually the success type called Right. That's a little abstract, so here's a concrete example:

// Just a class to hold a failure
case class DownloadFailure(msg: String)

def downloadUrl(url: String): Either[DownloadFailure, String] =
  if (url.isEmpty) {
    Left(DownloadFailure("Passed URI is empty"))
  } else {
    // Download the url, return the body as a String
    val body = getBody(url)
    Right(body)
  }

downloadUrl takes a String and returns an Either wrapping a DownloadFailure on the left or a String on the right.

So, assuming we've taken a list of urls and downloaded them:

val files = List(...)
val downloads = files.map(downloadUrl)

You can just handle successes:

val bodiesOver10Chars = downloads.right.filter(_.length > 10)

Or just log errors:

downloads.left.foreach(e => log.error("Bad download: " + e))

Or you could explicitly handle each with match:

for (download <- downloads) {
  download match {
    case Left(e) => log.error("Bad download: " + e)
    case Right(body) => println("Body was: " + body)
  }
}

But you can't forget that both Left and Right are possible. And if you turn out to have other types of failures that need to be handled in different ways, you can always split them into different failure classes. The following snippet updates status counters based on an Either returned from fetcher.fetch

    // fetcher.fetch looks like:
    // def fetch(uri: String): Either[Failure, Download]

    val result =
      if (! worthDownloading(uri)) {
        Left(NotWorthDownloading("didn't feel like it"))
      } else {
        downloadTimer.time { fetcher.fetch(uri) }
      }

    result match {
      case Right(download) => // Getting counted with the timer
      case Left(NotWorthDownloading(_)) => skippedJobs.mark()
      case Left(Unauthorized(_)) => authFailures.mark()
      case Left(BadResponse(_)) | Left(CurlError(_) | TooMany500s(_)) => failedDownloads.mark()
    }

Personally, I think this might be the right middle ground. Type systems force you to accept that failure can happen (something oft missed with return codes) without having to see it at every turn (a la checked exceptions).

And if you don't need data associated with failure cases, you can skip Either completely, and use Option.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment