Async patterns: Callbacks

So what is a callback function?

A callback function, also known as a higher-order function, is a function that is passed to another function (let’s call this other function “otherFunction”) as a parameter, and the callback function is called (or executed) inside the otherFunction. A callback function is essentially a pattern (an established solution to a common problem), and therefore, the use of a callback function is also known as a callback pattern. (Source)

A quick example of callback usage is the underscore.js _.each function, which iterates over a list of elements and executes a provided function (callback) on those elements in turn. Below, we have defined a function named logMe, which we will use as our callback in our example.

function logMe(element, index, list) {
  console.log('** new iteration **');
  console.log('   current element: ', element);
  console.log('   idx of current element: ', index);
}

_.each takes three arguments, (list, iteratee, [context]). We’ll worry about the first two. List is our collection (array or object) we want to iterate over, and iteratee is the callback function which will execute for each element in the collection.

We know from the documentation that each invocation of the callback we provide is called with three arguments:

  • If array: callback(element, index, list)
  • If object: callback(value, key, list)

Calling _.each(['a', 'b', 'c', 'd'], logMe) will log:

** new iteration **
   current element:  a
   idx of current element:  0
** new iteration **
   current element:  b
   idx of current element:  1
** new iteration **
   current element:  c
   idx of current element:  2
** new iteration **
   current element:  d
   idx of current element:  3

So our callback function, logMe, is passed to _.each(), and we trust the _.each() function, which we did not write, to invoke logMe on each iteration.

So taking a peek at the _.each() function definition itself…

  _.each = _.forEach = function(obj, iteratee, context) {
    iteratee = optimizeCb(iteratee, context);
    var i, length;
    if (isArrayLike(obj)) {
      for (i = 0, length = obj.length; i < length; i++) {
        iteratee(obj[i], i, obj);
      }
    } else {
      var keys = _.keys(obj);
      for (i = 0, length = keys.length; i < length; i++) {
        iteratee(obj[keys[i]], keys[i], obj);
      }
    }
    return obj;
  };

… it becomes more clear why and how exactly our callback is called with those three arguments.

How do callbacks help us manage asynchrony in JavaScript?

How do we write asynchronous code, and what do callbacks have to do with it? Much like the example above (which has abstracted away the mechanisms of iterating over a list and applying a callback on each iteration, and simply asks that we provide certain information in a certain way) often asynchronous code is done for us ‘by browser/server APIs (XMLHttpRequest, Node fs module) or third-party libraries (jQuery.ajax)’.

Let’s look at an example, and relate it to our understanding of the application of _.each, above.

(The following example is referenced from Jonathan Creamer’s ‘Event-Based Programming: What Async Has Over Sync‘ — more detail at the link).

var data;
  
$.ajax({
    url: "some/url/1",
    success: function( data ) {
        // But, this will!
        console.log( data );
    }
})
// Oops, this won't work...
console.log( data );

Why does the code we provide to the success callback work, but the outside console.log does not? Something is happening behind the scenes…

xmlhttp.open( "GET", "some/ur/1", true );
xmlhttp.onreadystatechange = function( data ) {
    if ( xmlhttp.readyState === 4 ) {
        console.log( data );
    }
};
xmlhttp.send( null );

The underlying XmlHttpRequest (XHR) object sends the request, and the callback function is set to handle the XHR’s readystatechange event. Then the XHR’s send method executes.

As the XHR performs its work, an internal readystatechange event fires every time the readyState property changes, and it’s only when the XHR finishes receiving a response from the remote host that the callback function executes. (Source)

Again, the mechanism by which our code is executed is abstracted away from us. Here, it’s an event handler that calls back the code we provide with the original call.

This is why the call is non-blocking — we can continue on in the thread until the event fires and queues up the callback.

What are the pitfalls of the callback pattern?

Check out Chapter 2 of Kyle Simpson’s ‘Async & Performance‘, for more detail — it is my primary source here and goes into greater depth. I have essentially attempted to summarize as a quick introduction below.

  1. Unnatural to reason about: We (as humans) think (consciously) in sequential, blocking ways, despite that our subconscious processes actually look quite asynchronous. This makes callbacks, which ‘express asynchronous flow in a rather nonlinear, nonsequential way’ much more unnatural for us to reason about, and difficult for beginners to digest.
  2. Inversion of control: As seen in the examples with underscore.js and jQuery AJAX, we are giving away control to another party, just trusting it to behave consistently and predictably, and hoping it does. Building in handling for all the things that could go wrong is imperfect — it bloats code and makes it harder to maintain, and even harder to reason about.

References / Resources

Leave a Reply

Your email address will not be published. Required fields are marked *