Promises instead of callbacks

20 January 2014

A few weeks ago I built up a very simple identity map to get rid of a bug in highlighting active links. I introduced promises in order to leverage the fact that Ember.js blocks in model hooks until the model is resolved (or rejected).

In this post, I am taking a step back, and converting all the ajax calls that fetch data from and store data to the backend to use promises. I am also going to extract the most basic adapter that exists, just to do away with the repetition in the XHR calls. Once I have these in place, I will able able to build nice features on top of that.

App.TheMostBasicAdapterThereIs

All calls go to the same backend and talk in json so these can be trivially extracted:

1App.Adapter = {
2  ajax: function(path, options) {
3    var options = options || {};
4    options.dataType = 'json';
5    return Ember.$.ajax('http://rock-and-roll-api.herokuapp.com' + path, options)
6  }
7}

With that out of the way, we can see where these were used, and replace Ember.$.ajax with App.Adapter.ajax. In the process we are also going to convert the callback-style code to use promises, a worthwhile transformation.

Don't call me back, promise me you'll be there

Here is what the code for fetching all the artists from the API looks like after applying the adapter change:

 1App.ArtistsRoute = Ember.Route.extend({
 2  model: function() {
 3    var artistObjects = [];
 4    App.Adapter.ajax('/artists', {
 5      success: function(artists) {
 6        artists.forEach(function(data) {
 7          artistObjects.pushObject(App.Artist.createRecord(data));
 8        });
 9      }
10    });
11    return artistObjects;
12  },
13  (...)
14});

Notice that the model initially is an empty array and only when the ajax call returns successfully does that array get filled up and the template rerendered. Not a big deal in itself, but if in a child route we rely on the array containing all the artists (e.g when looking up the identity map or using modelFor), we can be bitten by the async bug. Promises to the rescue.

As I mentioned in the identity map post, if a promise is returned from a model hook, Ember.js will block until the promise is resolved (or rejected). Let's follow in Ember.js footsteps and convert the above code to return a promise:

 1App.ArtistsRoute = Ember.Route.extend({
 2  model: function() {
 3    return Ember.RSVP.Promise(function(resolve, reject) {
 4      App.Adapter.ajax('/artists').then(function(artists) {
 5        var artistObjects = [];
 6        artists.forEach(function(data) {
 7          artistObjects.pushObject(App.Artist.createRecord(data));
 8        });
 9        resolve(artistObjects);
10      }, function(error) {
11        reject(error);
12      });
13    });
14  },
15  (...)
16});

We wrap the promise returned from the App.Adaptar.ajax call in another promise, which resolves with artist objects instead of the raw data that is returned by the API. In the rejection handler, we pass along any potential error responses by rejecting with the same error that we got.

Next, we do the same thing in the child route. We go from here:

 1App.ArtistsSongsRoute = Ember.Route.extend({
 2  model: function(params) {
 3    var artist = App.Artist.create();
 4    App.Adapter.ajax('/artists/' + params.slug, {
 5      success: function(data) {
 6        artist.setProperties({
 7          id: data.id,
 8          name: data.name,
 9          songs: App.Artist.extractSongs(data.songs, artist)
10        });
11      }
12    });
13    return artist;
14  },
15  (...)
16});

To here:

 1App.ArtistsSongsRoute = Ember.Route.extend({
 2  model: function(params) {
 3    return Ember.RSVP.Promise(function(resolve, reject) {
 4      App.Adapter.ajax('/artists/' + params.slug).then(function(data) {
 5        resolve(App.Artist.createRecord(data));
 6      }, function(error) {
 7        reject(error);
 8      });
 9    });
10  },
11  (...)
12});

To get the "100% promisified" seal, we'll transform the create calls, too. I'll only show the one to create an artist since creating a song is the same.

 1createArtist: function() {
 2  var name = this.get('controller').get('newName');
 3
 4  App.Adapter.ajax('/artists', {
 5    type: 'POST',
 6    data: { name: name },
 7    context: this
 8  }).then(function(data) {
 9      var artist = App.Artist.createRecord(data);
10      this.modelFor('artists').pushObject(artist);
11      this.get('controller').set('newName', '');
12      this.transitionTo('artist.songs', artist);
13  }, function(reason) {
14    alert('Failed to save artist');
15  });
16}

Here, there is not that much of a difference, the success and error callbacks are replaced by fulfillment and rejection handlers.

The source code with these changes can be got here.

Further studies & posts

You can acquire a deeper knowledge about promises by reading Domenic Denicola's "You're missing the point of promises" post and using that as a base for further exploration. Steven Kane's excellent promise-it-wont-hurt package makes you solve increasingly difficult challenges with promises, which is the best way to learn.

Promisifying all backend calls sets the stage for other routing-related improvements. Stay tuned for more.

Share on Twitter