Promises 102

Thomas Boyt @ Venmo

First, a refresher...

Managing asynchronous behavior is hard.

  • Example: let's search for a username and then make a payment to the search result.

Find the user


$.get('/api/search/kortina', function(payload) {
  var user = payload.data[0];
  alert('user id is ' + user.id);
});
$.get('/api/search/kortina', function(payload) {
  var user = payload.data[0];
   alert('user id is ' + user.id);
}, function(err) {
  alert('Hey, everything broke!');
});
function searchUser(username, cb, errCb) {
  $.get('/api/search/' + username, function(payload) {
    var user = payload.data[0];
    cb(user);
  }, err);
}

searchUser('kortina', function(user) {
  alert('user id is ' + user.id);
}, function(err) {
  alert('Hey, everything broke!');
});     

Find and pay the user

function searchUser(username, cb, err) {
  $.get('/api/search/' + username, function(payload) {
    var user = payload.data[0];
    cb(user);
  }, err);
}
function payUserId(id, amount, cb, err) {
  $.post('/api/v5/payments/', { user_id: 1, amount: amount }, function(payload) {
    cb(payload.balance);
  }, err);
}

searchUser('kortina', function(user) {
  payUserId(user.id, 1.00, function(balance) {
    alert('Made payment; your balance is now ' + balance);
  }, function() {
    alert('error making payment ' + err.reason);
  });
}, function(err) {
  alert('error finding user ' + err.reason);
});

Enter promises!

function searchUser(username) {
  return $.get('/api/search/' + username)
    .then(function(payload) {
      var user = payload.data[0];
      return user;
    });} 
function payUserId(id, amount) {
  return $.post('/api/v5/payments/', { user_id: 1, amount: amount })
    .then(function(payload) {
      return payload.balance;
    });
} 
searchUser('kortina')
  .then(function(user) {
    return payUserId(user.id, 1);
  }, function(err) {
    alert('error finding user ' + err.reason);
  })
  .then(function(balance) {
    alert('Made payment; your balance is now ' + balance);
  }, function(err) {
    alert('error making payment ' + err.reason);
  });

Promises 101

  • An asynchronous method (like $.get ) will return a promise object, which you can call .then(onFulfilled, onRejected)  on
  • The onFulfilled  callback will receive the value the promise was resolved  with as the argument, and the onRejected callback will receive the value the promise was rejected  with as the argument
  • Promises can be chained as many times as you want:
this.user.save()
  .then(this.user.setAccessTokenCookie.bind(this.user))
  .then(this.user.createDjangoSession.bind(this.user))
  .then(this.user.sendCode.bind(this.user))
  .then(this.handleSuccess, this.handleFailure); 
  • This is because .then   always returns a promise!

Promises 101

  • The onFulfilled callback can either return a promise or a value that will create an instantly-resolved promise with that value
  • For example:
function getNewestDiaryEntry() {
  return $.get('/api/secret')
    .then(function(payload) {
      // $.get returns a promise...
      return $.get('/api/my_secret_diary/' + payload.secret);
    });
    .then(function(payload) {
      // ...but here we just return a value...
      return payload.entries[0];
    })
}

// ...but we can still chain off of it!
getNewestDiaryEntry()
  .then(function(entry) {
    alert('Newest diary entry title: ' + entry.title);
  });

With that out of the way...

jQuery Deferreds aren't "real" promises!

  • In the previous examples, $.get and $.post didn't return "real promises"
  • jQuery Deferreds don't comply to the Promises/A+ spec, which is what every promise library in the world uses, as well as the promises in ES6 (the next version of JavaScript)
  • For details, see @domenic's blog post:
    http://domenic.me/2012/10/14/youre-missing-the-point-of-promises/
  • tl;dr: chaining off of jQuery deferreds does not work properly

Thankfully, the fix takes all of three characters

  • At Venmo, we use the Q promise library, which supports "casting" jQuery deferreds to real promises:
q($.get('/api/v5/me')).then(...);
  • As an added bonus: Q also contains lots of neat utilities we'll get into later

Error Handling

  • What happens in the following code?
q($.get('/api/v5/me'))
  .then(function(payload) {
    console.log('got user', pyload);  // whoops, typo'd payload!
  }, function(err) {
    alert('an error happened');
  });
  • The error gets silently swallowed.
  • This is because promise callbacks will catch errors so that they can reject them and call any subsequent rejection callback
  • In this case, there is no subsequent rejection callback, so the error disappears

A | Always
B | Be
R | Rethrowing

Error handling, fixed

q($.get('/api/v5/me'))
  .then(function(payload) {
    console.log('got user', pyload);  // whoops, typo'd payload!
  }, function(err) {
    alert('an error happened in $.get');
  })
  .then(null, function(err) {
    console.log('an error happened in the success callback');
    throw err;  // *rethrow* error so it's visible in console & stops execution
  });

Alternatively:

q($.get('/api/v5/me'))
  .then(function(payload) {
    console.log('got user', pyload);  // whoops, typo'd payload!
  }, function(err) {
    alert('an error happened in $.get');
  })
  .catch(function(err) {
    console.log('an error happened in the success callback');
    throw err;  // *rethrow* error so it's visible in console & stops execution
  });

Alternatively: use .done

  • As the name implies, you use .done at the end of a promise chain
  • It may seem like .then, but you cannot chain off of it
  • .done will not catch errors in the success callback, meaning they will be rethrown as you expect
q($.get('/api/v5/me'))
  .done(function(payload) {
    // this will actually error!
    console.log('got user', pyload);  // whoops, typo'd payload!
  }, function(err) {
    alert('an error happened in $.get');
  });
  • Caveat: .done is an addition to Promises/A+ specific to the Q library, and is not "portable." It may not exist in other promises libraries and does not exist in ES6 promises.

Advanced Promises Kung-Fu

Chaining off the same promise

  • You can .then multiple times off of the same promise
  • All of the handlers attached to a promise are handled in the order they're attached before any subsequent promises are are resolved
var xhr = q.delay(50);

xhr.then(function() {
  console.log('I get called first!');
}).then(function() {
  console.log('I get called third!');
});

xhr.then(function() {
  console.log('I get called second!');
});

q.all

q.all([
  get('/api/v5/me'),
  get('/api/v5/news')
]).then(function(results) {
  var user = results[0];
  var news = results[1];
  // ...
});

In ES6 as Promise.all()!

Also pairs nicely with q.spread:

q.all([
  get('/api/v5/me'),
  get('/api/v5/news')
]).spread(function(user, news) {
  // ...
});

Mock promises for testing

  • q('blah') creates a promise that is resolved immediately with 'blah' as its value
  • q.reject('blah') creates a promise that is rejected immediately with 'blah' as its reason/error

Creating Promise-friendly APIs

  • A function that can be asynchronous should always be asynchronous: see @izs's "Designing APIs for Asynchrony"
  • With promises, this is easy: any function that can be asynchronous should return a promise
  • For example, a function that returns either user object that's cached in memory a user object fetched from the API should have two paths:
    • If it's not loaded, fetch the user from the server and return a promise chaining off the AJAX request
    • If it is loaded, return an immediately-resolved promise with the value of the user object

Resources

promises 102

By Thomas Boyt

promises 102

  • 1,290