maxtaco's Blog Archive Pages Categories Tags

Handling Errors in IcedCoffeeScript

18 September 2014

Chris Coyne and I wrote IcedCoffeeScript (ICS) almost 3 years ago, and have been developing with it almost exclusively since. Our current project, Keybase.io, uses ICS everywhere: on the Web front-end, in our node command-line client, and on the server back-end. As client code becomes more complex, we find the features of ICS more crucial to getting our job done. I thought I’d summarize what makes it a nice system for those who haven’t checked it out.

Background

IcedCoffeeScript is a fork of CoffeeScript that introduced two new keywords — await and defer. Internally, it augments the CoffeeScript compiler with a Continuation-Passing Style (CPS) code rewriter. So the compiler outputs “pyramid-of-death” style spaghetti JavaScript code, while the programmer sees clean straightline CoffeeScript-like code.

For instance, consider this common pattern in node.js. I want to make two serial HTTP requests, and the first depends on the second. When it’s all done, I want a callback to fire with the results, or to describe that an error happened. Here’s the standard CoffeeScript way:

get2 = (cb) ->
  request "https://x.io/", (err, res, body) ->
    if err? then cb err
    else 
      request "https://x.io/?q=#{body.hash}", (err, res, body) ->
        if err? then cb err
        else cb null, body

This is not a contrived example, it was literally the first one I thought of. I hate this code for several reasons: it’s hard to read; it’s hard refactor; there are repeated calls to cb, any one of which might be forgotten and can break the program; it’s brittle and won’t compose well with standard language features, like if and for.

Cool that Coffee Down

The first part of the solution came intentionally with IcedCoffeeScript; use CPS conversion to achieve the illusion of threads:

get2 = (cb) ->
  await request "https://x.io/", defer(err, res, body)
  unless err?
    await request "https://x.io/?q=#{body.hash}", defer(err, res, body)
  cb err, body

It’s not crucial here to understand the finer points of await and defer, but the salient aspects are that the function get2 blocks until request completes, at which point err, res, body will get the three values that request called back with. Then, control continues at unless err?.

Already, this code is much cleaner. There’s only one call to cb; the code doesn’t veer off the page to the right; adding conditionals and iteration would be straightforward. But there’s still an issue: errors are ugly to handle. If we had 4 steps in this request pipeline, we’d need three checks of err? to make sure there wasn’t an error. Or something equally gross:

get2 = (cb) ->
  await request "https://x.io/", defer(err, res, body)
  return cb err if err?
  await request "https://x.io/?q=#{body.hash}", defer(err, res, body)
  return cb err if err?
  # etc...
  cb null, res


An Elegant Solution that Exploits the CPS-conversion

An elegant solution was discovered a year into writing code with IcedCoffeeScript. In the above example, the language feature defer(err, res, body) creates a callback that request calls when it’s done. That callback represents the rest of the get2 function! Meaning, if there’s an error, it can be thrown away, since the rest of the function should not be executed. Instead, the outer callback, cb, should be called with an error.

We can accomplish this pattern without language additions, just with library help:

{make_esc} = require 'iced-error'
get2 = (cb) ->
  esc = make_esc cb # 'ESC' stands for Error Short Circuiter
  await request "https://x.io/", esc(defer(res, body))
  await request "https://x.io/?q=#{body.hash}", esc(defer(res, body))
  cb null, body

If the first call to request calls back with an error, then cb is called with the error, and the function is over. Otherwise, onto the second request (and the rest of get2). The function make_esc might seem magical, but it’s not, it’s doing something quite simple:

make_esc = (cb1) -> (cb2) -> (err, args...) ->
  if err? then cb1(err) else cb2(args...)

It takes the original callback, and returns a function that we’ve called esc. In turn, esc takes the local callback, and returns a third callback. This third callback is what request calls when it’s done. If the result is an error, then fire the outer callback with the error, throwing away cb2, which represents the rest of the function. If the result is not an error, then forge ahead. Call cb2 which executes the rest of the function.

This make_esc function is extremely useful and I use it in almost every function that I write. But because it’s a library function, you can write your own to work around the error semantics of your particular library, without having to fiddle with the IcedCoffeeScript compiler. Or, you can write an make_esc that first releases a lock, and then calls cb (also quite useful). Whatever ought to happen on error but before the function scope disappears, a short-circuiter can handle it cleanly.

In Sum…

IcedCoffeeScript plus the “Error Short-Circuiter” pattern is a powerful and succinct way to clean up your JavaScript-based applications. We’ve been writing code this way for over a year now and can’t imagine going back to the old toolset.