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.
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
.
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 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.
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.