The Callback Pattern


This is the first article from a new series about Node.js. We begin by covering some basics like asynchronous flow control, modules, event emitters and streams. We then proceed to some more elaborate patterns for providing configuration, networking, asynchronous work distribution and data persistence.

In this first article we cover the fundamental pattern for controlling flow in Node.js: the callback.

Enjoy!

- Pedro Teixeira, CTO, YLD!

Given its async nature, JavaScript code usually relies on functions for flow control when doing I/O. Typically, if you have a function that has to wait on a result — say, a response from a server in the network — you must provide a function to be called back. For instance, if you’re doing some browser jQuery code to perform an HTTP GET request, you could do:

$.get('http://some.server.com/some/url', function success(data) {  
  // do something  
});

But what happens when the request, for some reason, is not able to succeed? If you wanted to get notified of the errors you would have to fall back on the more powerful $.ajax method:

var settings = {    
  success: function success(data) {  
    // do something  
  },  
  error: function error(req, status, error) {  
    // handle error  
  }  
};
$.ajax('http://some.server.com/some/url', settings);

In the error-handling function, in order to understand more details about the origin of the error, you would have to parse the status argument of the error callback, which can have the values “timeout”, “error”, “abort”, “parseerror” or even null.

One of the problems with this interface is that it’s hard to compose. If you wanted to perform another HTTP request right after this request succeeded, your code would look indented and hard to read:

var settings = {    
  success: function success(data) {  
    var settings2 = {  
      success: function success2(data2) {  
        // success, handle the results data and data2  
      },  
      error: function error2(req, status, error) {  
        // handle error 2  
      }  
    };
    $.ajax('http://some.server.com/some/other/url', settings2);  
  },  
  error: function error(req, status, error) {  
    // handle error  
  }  
};
$.ajax('http://some.server.com/some/url', settings);

To make this more manageable we could decrease two levels of indentation by not inlining the callback functions:

function handleSuccess() {
  function handleSuccess2(data2) {  
    // success, handle the results data and data2  
  }
  function handlError2(req, status, error) {  
    // handle error 2  
  }
  var settings2 = {  
    success: handleSuccess2,  
    error: handlError2  
  };
  $.ajax('http://some.server.com/some/other/url', settings2);  
}
function handleError(req, status, error) {    
  // handle error  
}
var settings = {    
  success: handleSuccess,  
  error: handleError  
};
$.ajax('http://some.server.com/some/url', settings);

But still, simple I/O is obviously hard to compose, and we have yet to handle an occurring error.

What if the second call was not dependent on the result of the first one? You could do them in parallel, and the code to handle them would look something like this:

function handleSuccess(responses) {    
  // handle responses per URL here  
}
function handleError() {    
  // handle error  
}
var urls = \[    
  'http://some.server.com/some/url',  
  'http://some.server.com/some/other/url'  
\];
var responses = {};
checkFinished() {    
  if (Object.keys(responses) == urls.length) {  
    handleSuccess(responses);  
  }  
}  

urls.forEach(function(url) {    
  function handleSuccess(data) {  
    responses\[url\] = data;  
    checkFinished();  
  }
  var settings = {  
    success: handleSuccess,  
    error: handleError  
  };
  $.ajax(url, settings);  
});

That’s a lot of machinery needed just to make a couple of parallel requests. What patterns could make handling I/O easier?

The Callback Pattern

Unlike the previous example that used jQuery where there may be several callback functions involved in one call — one for error and one for success — there’s a pattern that Node.js and a lot of third-party libraries use to make it easy to compose different types of I/O calls.

This pattern consists of these principles:

  • One callback to rule them all: one sole function instead of a success and an error one;
  • The callback is called once and exactly once. It’s called when an error occurs, or a result is available;
  • Function-last: the function is the last argument of the call that initiates I/O;
  • Error-first: the callback has the following function signature:
function (err, result) {    
  //...  
}

The values passed into the callback have different values depending on whether an error occurred or not:

If no error occurred:

  • The first callback argument is null or undefined;
  • The second argument contains the callback results.

On the other hand, if an error occurred:

  • the first argument contains an error object, an instance of the JavaScript Error class (or sub-class) that describes the error;
  • the second argument will be undefined.

This patterns forces the programmer to not ignore errors (by not defining an error callback) and, more importantly, defines a common pattern on top of which you can build abstractions and utilities.

Here is an example of the callback pattern in action in Node.js when reading the contents of a file:

var fs = require('fs');
fs.readFile('/some/path/to/file.txt', function(err, fileContent) {    
  if (err) {  
    handleError(err);  
  } else {  
    // use fileContent  
  }  
});
function handleError(err) {    
  console.error('error caught while reading file:', err);  
}

Here we’re handling occurring errors simply by logging into the console, but depending on your application you may want to handle it more appropriately: log it into a central place, notify someone, terminate a request, etc.

The important part of the callback function is that we’re breaking the flow of the application. Instead of using an if/else branch for that, you may also use a return, which would go like this:

fs.readFile('/some/path/to/file.txt', function(err, fileContent) {    
  if (err) {  
    return handleError(err);  
  }  
  // use fileContent  
});

Next article

So far we’ve covered the fundamental pattern for controlling flow in Node.js: the callback.
In the next article we’ll discuss how we can further use callbacks to coordinate asynchronous operations.

Written by: Pedro Teixeira

Originally published at blog.yld.io on October 19, 2015.


Written by YLDOctober 1st, 2015


Share this article