JavaScript Callbacks: The Function is Last… or Lost?

Aug 22 2012

In JavaScript -- especially of the Node.js sort -- it is a common pattern to put a functional callback as the last argument in a parameter list. For example, we might define a function that looks like this:

/**
 * @param {String} data
 *   Data to save.
 * @param {Object} properties
 *   Properties to be added to the object.
 * @param {Function} fn
 *   A callback called when the object is saved.
 */
function save(data, properties, fn) {
  // Do something with data and properties

  fn('All done');
}

What do you do, though, if properties is optional? <!--break--> The prevailing wisdom in many cases is to move optional parameters to the end of the list. But this conflicts with the JavaScript pattern of always putting the callback last. And the latter pattern leads to more readable code. Yet if we follow that pattern, we end up with something like this:

function save(data, properties, fn) {
  if (fn == undefined && typeof properties == 'function') {
    fn = properties;
    properties = undefined;
  }

  // Do something with data and properties

  fn('All done');
}

This gets even more confusing if we were to add another optional parameter.

While I haven't found an ideal solution, I found one that at least makes it easy to keep the last argument the Function, while allowing internal arguments to be optional. The basic idea is to regenerate the arguments list, moving the Function to its assumed place, while setting interior optional arguments to undefined, as they should be.

Here is a function that does this:

function argsWithFn(args, names) {
  var newArgs = {};
  var found = false;

  // Loop through the arguments, starting from the end, and
  // find the function.
  for (var i = args.length - 1; i >= 0; --i) {

    // If we find the function, put it in its expected place
    // at the end of the arguments list.
    if (!found && typeof args[i] == 'function') {
      var fn = args[i];
      newArgs[names[i]] = undefined;
      newArgs[names[names.length - 1]] = fn;
      found = true;
    }

    // Otherwise, just store the value in the new arguments.
    else {
      newArgs[names[i]] = args[i];
    }
  }
  return newArgs;
}

This function takes an arguments list (usually the arguments object) and a list of the names of all of the expected parameters. And it returns a new object that contains all of the arguments.

Now we can use the code above like this:

function save(data, properties, fn) {
  var newArgs = argsWithFn(arguments, ['data', 'properties', 'fn']);

  // We could do this, but it's not required.
  data = newArgs.data;
  properties = newArgs.properties;
  fn = newArgs.fn;

  // Do something with data and properties

  newArgs.fn('All done');
}

This code uses the JavaScript built-in arguments object to fetch a list of all of the passed-in function parameters. It provides a list of names for the parameters it wants back. And what it gets back is a full list of parameters, with any empty optional parameters having an undefined value, while the last parameter (fn) points to the Function object.