When to use it: JavaScript Promises

Let’s say you’re developing a simple alternative to the TriMet  website, where riders can (among other things) check real-time location of buses and trains.  This means that your website will need to query the TriMet TransitTracker service when one of your site’s users performs a search.

So now your life has many more problems than just a moment ago.  How does your code get called when the TransitTracker service:

  • returns the real-time location information?
  • is unavailable?

Further, how do you implement your site so that it shows on-time arrivals in green, and late arrivals in red?

Legacy answer

The legacy answer to these types of questions is that we would use callbacks attached to event handlers.  The success and failure callbacks would be passed to the function that queried the TransitTracker service.  With an XMLHttpRequest, the code could look something like this (reposted from MDN):


function makeRequest(url) {
  httpRequest = new XMLHttpRequest();
  if (!httpRequest) {
    alert('Giving up! Cannot create an XMLHTTP instance');
    return false;
  }

  httpRequest.onreadystatechange = alertContents;
  httpRequest.open('GET', url);
  httpRequest.send();
}

function alertContents() {
  try {
    if (httpRequest.readyState === XMLHttpRequest.DONE) {
      if (httpRequest.status === 200) {
        alert(httpRequest.responseText);
      }
      else {
        alert('There was a problem with the request.');
      }
    }
  }
  catch( e ) {
    alert('Caught Exception: ' + e.description);
  }
}

You can already see problems – this shows an alert box for success or error.  How could you write a reusable request function that did a custom action on success and failure?  We could refactor it into this, to improve it a little bit:


function makeRequest(url, success, failure) {
  var httpRequest = new XMLHttpRequest();
  if (!httpRequest) {
    alert('Giving up!  Cannot create an XMLHTTP instance');
    return false;
  }

  var makeReadystatechangeFn = function(success, failure) {
    return function() {
      try {
        if (httpRequest.readyState === XMLHttpRequest.DONE) {
          if (httpRequest.status === 200) {
            success(httpRequest.responseText);
          }
          else {
            failure();
          }
        }
      }
      catch( e ) {
        failure(e);
      }
    };
  };

  httpRequest.onreadystatechange = makeReadystatechangeFn(success, failure);
  httpRequest.open('GET', url);
  httpRequest.send();
}

function getArrivalTimes() {
  var success = function(responseText) {
    // parse responseText and show arrival time on the page in green or red
  };
  var failure = function(ex) {
    // show a toast, possibly using ex if it's defined
  };

  var transitTrackerUrl;
  // create the proper transitTrackerUrl...

  makeRequest(transitTrackerUrl, success, failure);
}

Hooray!  Now we can make a request and do something on success, or something else on failure.  All we have to do is write the success callback so that it displays the on-time arrivals in green, and the late arrivals in red.  Job done, right?

Not really.  What if you want to show service alerts when the arrival is late or TransitTracker is down?  Remember, in a mobile-first environment where connections might be metered or unreliable, it’s best to only perform network queries when necessary.  So, ideally we’d perform this query in sequence with the previous query, rather than in parallel.

I can hear your thoughts through the internet right now: “Pfft, just write the success callback so that if the arrival time is late, it performs another makeRequest() call.  Also, modify the original failure handler so that it performs another makeRequest() call.”  Like this:

function getArrivalTimes() {
  var serviceAlertsSuccess = function(responseText) {
    // parse responseText and show any service alerts
  };

  var serviceAlertsFailure = function(ex) {
    // show a toast, possibly using ex if it's defined
  };

  var getServiceAlerts = function() {
    // var serviceAlertsUrl = someTriMetUrl;
    makeRequest(serviceAlertsUrl, serviceAlertsSuccess, serviceAlertsFailure);
  };

  var success = function(responseText) {
    // parse responseText and show arrival time on the page in green or red
    if (arrivalTime is late) {
      getServiceAlerts();
    }
  };
  var failure = function(ex) {
    // show a toast, possibly using ex if it's defined
    getServiceAlerts();
  };

  var transitTrackerUrl;
  // create the proper transitTrackerUrl...

  makeRequest(transitTrackerUrl, success, failure);
}

OK, job done again.  Though it’s still not great – it’s already showing signs of being hard to read, even though almost all the code is condensed into comments and there’s only 2 AJAX operations.  If this were your first day on the job, and your manager handed you fully implemented code that implemented this process, how long would it take you to understand it?

It gets worse though – what if you want to automatically send an email to TriMet IT whenever the TransitTracker or service alerts are down?  We’d add yet another set of functions, and call them in multiple places, and have even more spaghetti code.  And as we add more of these features (ex: show an ad for nearby rentals only if the bus is late and there are no service alerts) this code only becomes harder to understand and extend.

The code written above is one actualization of “callback hell” – the situation where you call a function that has callbacks, and the callbacks have callbacks, and those callbacks have callbacks, and so on.  It’s a little more typically written as the “pyramid of doom,” like below. Either way it’s difficult to read, understand, and debug:

function(param1, function() {
  otherFn1(param2, function() {
    otherFn2(param3, function() {
      ...
    });
  }, function() {
    otherFn3(paramX, function() {
  });
});

Root cause

The type of code that we’re talking about above has the form of

synchronous operation ⇒ asynchronous operation ⇒ synchronous operation ⇒ asynchronous operation ⇒ synchronous operation ⇒ etc

Where “synchronous operation” refers basically to code that must complete before the next statement executes.  “Asynchronous operation” refers to code that allows the next statement to execute before it is complete, and it completes at an unpredictable time in the future.

In our example, synchronous operations include (but are not limited to):

  1. Parsing responseText
  2. Modifying the DOM so the arrival time is displayed on the page in green or red
  3. Displaying toasts
  4. Modifying the DOM to show service alerts

Asynchronous operations include:

  1. Requesting data from the transitTrackerUrl
  2. Requesting data from the serviceAlertsUrl
  3. Sending email to TriMet IT
  4. Getting an ad

So the problem is that we are attempting to chain together interleaved synchronous and asynchronous operations, but the chaining method (callbacks inside of callbacks) does not clearly communicate the developer intent.  Also, because every asynchronous operation has 2 possible outcomes – success and fail – the callback graph looks more like a tree than a line, which makes the program flow harder to understand.

This situation of integrating asynchronous and synchronous operations is when to use Promises.

Promises

A Promise, specifically the standard built-in promise, is designed to make the integration of synchronous and asynchronous operations easier to understand and less error prone.

A good rule of thumb for getting started with using promises is this: create a new promise for each asynchronous operation, and use the then() function to specify the synchronous operations that should occur after the asynchronous operation succeeds.

A promise is created by providing it with a function that starts an asynchronous operation, which is called the executor function.  The executor takes 2 parameters – a fulfillment function, and a rejection function.  After the asynchronous operation completes, the promise calls the fulfillment or rejection function.  If the asynchronous operation succeeds, the promise calls the fulfillment function and passes it the results of the asynchronous operation.  If the asynchronous operation fails, the promise calls the rejection function.  The executor function notifies the promise of success by calling the fulfill() function, or of failure by calling the reject() function.  Check this out:

function makeRequestPromise(url) {
  return new Promise(function(fulfill, reject) {
    var httpRequest = new XMLHttpRequest();
    if (!httpRequest) {
      reject('Giving up! Cannot create an XMLHTTP instance');
      return;
    }
    var processContents = function() {
      try {
        if (httpRequest.readyState === XMLHttpRequest.DONE) {
          if (httpRequest.status === 200) {
            fulfill(httpRequest.responseText);
          }
          else {
            reject('There was a problem with the request.');
          }
        }
        catch( e ) {
          reject('Caught Exception: ' + e.description);
        }
      };
      httpRequest.onreadystatechange = processContents;
      httpRequest.open('GET', url);
      httpRequest.send();
    };
  };
}

Looks pretty similar, right?  But we are actually gaining a lot, even without using fetch(), which is another topic in itself.

  1. Rather than calling a failure callback directly, we’re just notifying the promise that the asynchronous operation has failed by calling reject().
  2. Rather than calling a success callback directly, we’re just notifying the promise that the asynchronous operation succeeded by calling fulfill().  We’re also providing the promise the successful result of the asynchronous operation.

If we’re using the standard built-in promise, we would specify the success and failure callbacks using the then() function.  Now our code that only gets real-time arrival data reduces to:

makeRequestPromise(transitTrackerUrl)
.then(function(responseText) {
  // parse responseText and show arrival time in green or red
}, function(err) {
  // show a toast using err as the message
});

Again not obviously an improvement.  Rather than passing callbacks to makeRequest, we’ve just put them in the then() call.  But we’re about to see some real benefits when we start chaining other asynchronous and synchronous operations.  When then() returns a promise, we can chain then()s together, making the program flow very obvious.  We can also specify a single catch() function that will handle all rejections from anywhere in the chain.  Check out this implementation of checking the service alerts:

makeRequestPromise(transitTrackerUrl)
.then(function(responseText) {
  // parse responseText and show arrival time in green or red
  return Promise.resolve(arrivalTime)
}, function(err) {
  // there was an error getting real-time updates.  Indicate that
  // we need to get service alerts by setting a null arrivalTime
  return Promise.resolve(null)
}).then(function(arrivalTime) {
  // Get service alerts if late or RT data is unavailable
  return (arrivalTime is late or null)
    ? makeRequestPromise(serviceAlertsUrl)
    : Promise.resolve(null);
}).then(function(serviceRequestResults) {
  if (!serviceRequestResults) return; // not late
  // parse serviceRequestResults for service alerts and display them in DOM
}).catch(function(err) {
  // There was an error getting service requests.
  // show a toast with err as the message
});

This code has the same program flow as the last code sample in the legacy section, but this code’s program flow is pretty clear.  It’s also easy to see how the operations could be chained further.

I would like to note that there’s actually a better implementation of my makeRequestPromise() function available – fetch(). There’s a nice polyfill available for it in case you need to support browsers without it built-in.

There’s a lot more to promises, but I think this gives a good idea of when to use promises: when you need to integrate asynchronous and synchronous operations.

Please leave any comments below!

Leave a Reply

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