Tony Lukasavage

Caffeine. Whiskey. Code. Mostly the last one.

Callback Spotting: Flexible APIs in Javascript

| Comments

Callbacks are a necessity when developing an asynchronous API in node.js. No, really, I promise. To that end, here’s a little trick I originally spotted in the node.js source code. When creating a function that will require a callback, but has a flexible invocation signature, I always use the maybeCallback() function.

1
2
3
4
5
6
7
8
// you can use lodash or underscore for this
var isFunction = function(o) {
  return Object.prototype.toString.call(o) === '[object Function]';
}

function maybeCallback(callback) {
  return isFunction(callback) ? callback : function(err) { throw err; }
}

Essentially, if you give it a function, it gives you the function back. Otherwise, it gives you back a new function that quietly does nothing, unless there’s an error, in which case it throws that error. Simple, right?

In node.js, it is a standard convention that the callback for an asynchronous function is the last argument. This is simple enough with concrete APIs, like say fs.stat:

fs.stat(path, callback)

There’s always a path, and always a callback. No trickery needed in the API creation. But what about an API like fs.readFile?

fs.readFile(filename, [options], callback)

There’s always a filename, there might options, and there should be a callback. So how exactly might we structure this under-the-hood? I won’t regurgitate the node.js source code here, but let’s instead see how we might use maybeCallback to set up this API.

1
2
3
4
5
6
7
8
function readFile(filename, options, callback) {
  callback = maybeCallback(arguments[arguments.length-1]);
  if (!options || isFunction(options)) {
      options = { /* default values */ };
  }

  // do stuff
}

Those few lines of code do the following:

  • Use the arguments object to find the last argument that was provided to the function.
  • Use maybeCallback() on the last argument to get our callback.
  • If options doens’t exist, or is a function (implying that it’s our callback), do your default processing of the options.

This is a common pattern that I use all the time when creating APIs. And giving the users of your modules this flexibility is often key to not just their success, but also their delight in using your code. More users, more feedback, more pull requests, better module. You know how it goes.

Comments