The Node.js Way - Understanding Error-First Callbacks

March 12, 2014 (Updated December 18, 2014)

If Google’s V8 Engine is the heart of your Node.js application, then callbacks are its veins. They enable a balanced, non-blocking flow of asynchronous control across modules and applications. But for callbacks to work at scale you’ll needed a common, reliable protocol. The “error-first” callback (also known as an “errorback”, “errback”, or “node-style callback”) was introduced to solve this problem, and has since become the standard for Node.js callbacks. This post will define this pattern, its best practices, and exactly what makes it so powerful.

Why Standardize?

Node’s heavy use of callbacks dates back to a style of programming older than JavaScript itself. Continuation-Passing Style (CPS) is the old-school name for how Node.js uses callbacks today. In CPS, a “continuation function” (read: “callback”) is passed as an argument to be called once the rest of that code has been run. This allows different functions to asynchronously hand control back and forth across an application.

Node.js relies on asynchronous code to stay fast, so having a dependable callback pattern is crucial. Without one, developers would be stuck maintaining different signatures and styles between each and every module. The error-first pattern was introduced into Node core to solve this very problem, and has since spread to become today’s standard. While every use-case has different requirements and responses, the error-first pattern can accommodate them all.

Defining an Error-First Callback

There’s really only two rules for defining an error-first callback:

  1. The first argument of the callback is reserved for an error object. If an error occurred, it will be returned by the first err argument.
  2. The second argument of the callback is reserved for any successful response data. If no error occurred, err will be set to null and any successful data will be returned in the second argument.

Really… that’s it. Easy, right? Obviously there are some important best practices as well, but before we dig into those lets put together a real-life example with the basic method fs.readFile():

fs.readFile('/foo.txt', function(err, data) {
  // TODO: Error Handling Still Needed!
  console.log(data);
});

fs.readFile() takes in a file path to read from, and calls your callback once it has finished. If all goes well, the file contents are returned in the data argument. But if somethings goes wrong (the file doesn’t exist, permission is denied, etc) the first err argument will be populated with an error object containing information about the problem.

Its up to you, the callback creator, to properly handle this error. You can throw if you want your entire application to shutdown. Or if you’re in the middle of some asynchronous flow you can propagate that error out to the next callback. The choice depends on both the situation and the desired behavior.

fs.readFile('/foo.txt', function(err, data) {
  // If an error occurred, handle it (throw, propagate, etc)
  if(err) {
    console.log('Unknown Error');
    return;
  }
  // Otherwise, log the file contents
  console.log(data);
});

Err-ception: Propagating Your Errors

When a function passes its errors to a callback it no longer has to make assumptions on how that error should be handled. readFile() itself has no idea how severe a file read error is to your specific application. It could be expected, or it could be catastrophic. Instead of having to decide itself, readFile() propagates it back for you to handle.

When you’re consistent with this pattern, errors can be propagated up as as many times as you’d like. Each callback can choose to ignore, handle, or propagate the error based on the information and context that exist at that level.

if(err) {
  // Handle "Not Found" by responding with a custom error page
  if(err.fileNotFound) {
    return this.sendErrorMessage('File Does not Exist');
  }
  // Ignore "No Permission" errors, this controller knows that we don't care
  // Propagate all other errors (Express will catch them)
  if(!err.noPermission) {
    return next(err);
  }
}

Slow Your Roll, Control Your Flow

With a solid callback protocol in hand, you are no longer limited to using one callback at a time. Callbacks can be called in parallel, in a queue, in serial, or any other combination you can imagine. If you want to read in 10 different files, or make 100 different API calls, there’s no reason to make them one-at-a-time.

The async library is great for advanced callback usage. And because of the error-first callback pattern, it’s incredibly easy to hook in to.

// Example taken from caolan/async README
async.parallel({
    one: function(callback){
        setTimeout(function(){
            callback(null, 1);
        }, 200);
    },
    two: function(callback){
        setTimeout(function(){
            callback(null, 2);
        }, 100);
    }
},
function(err, results) {
    // results is equal to: {one: 1, two: 2}
});

Bringing it all Together

To see all these concepts come together, check out some more examples on Github. And of course, you can always choose to ignore all of this callback stuff and go fall in love with promises… but that’s a whole other post entirely :)