Promises, Promises

An exploration of Promises/A+ using RSVP.js

Presented by Cory Forsyth / @bantic

What are promises?

“ A promise represents the eventual result of an asynchronous operation. The primary way of interacting with a promise is through its `then` method, which registers callbacks to receive either a promise’s eventual value or the reason why the promise cannot be fulfilled. ”

From http://promisesaplus.com/

Promise-based Code

What does it look like?


var successHandler = function(successValue){
  console.log("I am called when the promise successfully resolves.");
};

var rejectionHandler = function(reason){
  console.log("I am called when the promise is rejected for 'reason'.");
};

// doSomethingAsync() returns a Promise
doSomethingAsync().then( successHandler, rejectionHandler );
          

What are promises? Part 2

A pattern in javascript for coordinating asynchronous activity.

Examples:

  • Open a connection to a database, then do something else.
  • Load some JSON, then do something with the result.

Other patterns for dealing with asynchronicity

  • Callbacks
  • Event-based

Example callback-based code


connectToDb(
  function(connection) { // got connection
    connection.doSomething();
  }, function(err) {     // got error
    console.error("Could not connect to db: " + err);
  }
);
            

Example evented code


var transferComplete = function(){
  var json = JSON.parse(this.responseText)
  console.log('transferComplete, user name is: ' + json.name);
};

var transferFailed = function(){
  console.error('transferFailed');
};

var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.github.com/users/bantic', true);
xhr.addEventListener("load", transferComplete, false);
xhr.addEventListener("error", transferFailed, false);

xhr.send();
            

Cool Things About Promises

  • Flattening 'rightward drift'

Flattening rightward drift

Simple callback-based code

Callback version


connectToDb(
  function(connection) { // got connection
    connection.doSomething();
  }, function(err) {     // got error
    console.error("Could not connect to db: " + err);
  }
);
            

Promise version


connectToDb().then(
  function(connection){ // got connection
    connection.doSomething();
  }, function(err) {    // got error
    console.error("Could not connect to db: " + err);
  }
);
            

Flattening rightward drift

Nested callback-based code


connectToDb(function(connection) { // got connection
  connection.doSomething(function(result) {
      result.name = 'new name';
      result.save(function(){
        console.log('saved successfully');
      }, function(err){
        console.error('error saving');
      });
    }, function(err) {
      console.error("Error doing something");
    })
  }, function(err) {     // got error
    console.error("Could not connect to db: " + err);
  }
);
            

Flattening rightward drift

Nested callback-based code

Using promises


connectToDb().then(function(connection){ // connectToDb returns a promise
    return connection.doSomething();     // connection.doSomething returns a promise
}).then(function(result){
    result.name = 'new name';
    return result.save();                // result.save returns a promise
}).then(function(){
   console.log('saved successfully.');
 }, function(err){
   console.error("Error.");
 }
);
            

Cool Things About Promises

  • Flattening 'rightward drift'
  • Linearizing code

Linearizing code

Evented Code Part 1


var App = function(){
  this.init = function(){
    this.loadData();
  };

  this.start = function(data){
    console.log("Started for user: " + data.name);
  };

  // many more lines ...
  // How does the app actually start?
};
            

Linearizing code

Evented Code Part 2


var App = function(){
  this.init = function(){
    this.loadData();
  };
  this.start = function(data){
    console.log("Started for user: " + data.name);
  };
  // ... many lines later
  // ... oh, loadData calls start

  this.loadData = function(){
    var that = this;
    var xhr = new XMLHttpRequest();
    xhr.open('GET', 'https://api.github.com/users/bantic', true);
    xhr.addEventListener('load', function(){
      var data = JSON.parse(this.responseText);
      that.start(data);
    }, false);
    xhr.send();
  };
};
            

Linearizing Code

Promises simplify Evented Code


var App = function(){
  this.init = function(){
    var that = this;
    this.loadData().then( function(data) {
      that.start(data);
    };
  };
  this.start = function(data){
    console.log("Started for user: " + data.name);
  };
  // ... many lines

  this.loadData = function(){
    return new RSVP.Promise( function(resolve, reject){
      var xhr = new XMLHttpRequest();
      xhr.open('GET', 'https://api.github.com/users/bantic', true);
      xhr.addEventListener('load', function(){
        var data = JSON.parse(this.responseText);
        resolve(data);
      }, false);
      xhr.send();
    });
  };
};

            

Cool Things About Promises

  • Flattening 'rightward drift'
  • Linearizing code
  • Simple error handling

Simple error handling

Promise Resolution Spec


promise2 = promise1.then(onFulfilled, onRejected);
            
2.2.7.2 If either onFulfilled or onRejected throws an exception e, promise2 must be rejected with e as the reason.
2.2.7.4 If onRejected is not a function and promise1 is rejected, promise2 must be rejected with the same reason.

Simple error handling

Rejected promises propagate through the chain

When any promise in a chain of promises throws an exception, that promise will be rejected and the rest in the chain will be rejected for the same reason.

The rejection "falls through" until there is a rejection handler.

Simple error handling

Exceptions propagate to the next available error handler


var rejectedPromiseHandler = function(reason){
  console.error("Rejected with reason: " + reason);
};

doSomethingAsync()              // gets called
  .then( doSomethingAsync )     // gets called
  .then( function(){ throw new Error("Oops!"); } )
  .then( doSomethingAsync )     // never gets called,
                                // and since there is no rejection handler,
                                // the rejection 'falls through' to:
  .then( null, rejectedPromiseHandler ); // rejectedPromiseHandler gets called
                                         // with the thrown exception as its argument
            

Simple error handling

Rejected promises also propagate to the next available error handler


var rejectedPromiseHandler = function(reason){
  console.error("Rejected with reason: " + reason);
};

doSomethingAsync()              // gets called
  .then( doSomethingAsync )     // gets called
  .then( function(){ return RSVP.reject('rejected'); } )
  .then( doSomethingAsync )     // never gets called,
                                // and since there is no rejection handler,
                                // the rejection 'falls through' to:
  .then( null, rejectedPromiseHandler ); // rejectedPromiseHandler gets called
                                         // with argument 'rejected'
            

Neat things in RSVP.js

  • `.fail()`

These are niceties provided by RSVP.js and not part of the Promises/A+ spec

RSVP.js .fail

An alias for .then(null, errorHandler)


// this:
doSomethingAsync()
  .then( doSomethingElseAsync )
  .then( null, rejectionHandler );

// Is the same as this:
  doSomethingAsync()
    .then( doSomethingElseAsync )
    .fail( rejectionHandler );
            

Neat things in RSVP.js

  • `.fail()`
  • `.rethrow()`

Normally, a rejected promise will be handled by the first rejection handler. A thrown exception will be caught and handled by the next rejection handler.


doSomethingAsync()
  .then( function(){ throw new Error('error');} )
  .then( null, function(e){
    console.error("The error is caught and handled here.");
    throw e;
  })
  .then( null, function(e){
    console.error("I will execute also, because e was thrown.");
  });
            

Neat things in RSVP.js

  • `.fail()`
  • `.rethrow()`

But if you use RSVP.rethrow() then the error makes it out of the promise chain, where you can see it in your console.


doSomethingAsync()
  .then( function(){ throw new Error('error');} )
  .then( null, function(e){
    console.error("The error is caught and handled here.");
    RSVP.rethrow(e);
  });

  // the thrown error will
  // exit the promise chain and
  // the console will now display this error
            

This can be very helpful in debugging.

How to use promises in your own code

  1. Include rsvp.js from https://github.com/tildeio/rsvp.js
  2. Return a new RSVP.Promise from your async code
  3. Where you would have used a callback or event, use `resolve()` or `reject()` instead.

How to use promises in your own code

Example async code


var async = function(callback){
  setTimeout( function(){
    callback();
  }, 1000);
};

async( function(){
  console.log("This happens after 1000ms");
});
            

How to use promises in your own code

Example async code using rsvp.js


var asyncWithPromise = function(){
  return new RSVP.Promise( function(resolve, reject){
    setTimeout( function(){
      resolve();
    }, 1000);
  });
};

asyncWithPromise().then( function(){
  console.log("This happens after 1000ms");
});
            

How to use promises in your own code

Chaining promises

Gotcha: Make sure you return a promise if the code is async.


  asyncWithPromise().then( function(){     // happens first
    return asyncWithPromise();             // happens after the first finishes
  }).then( function() {
    return asyncWithPromise();             // happens after the second finishes
  });
            

How to use promises in your own code

Chaining promises with non-promise return values

It is ok to return non-promise values from non-async code.


  asyncWithPromise().then( function(){     // happens first
    doSomethingSynchronous();
    return someValue;                      // not returning a promise
  }).then( function() {
    return asyncWithPromise();             // returns a promise, which
                                           // gets assimilated into the chain.
                                           // The next 'then' doesn't fire until
                                           // this promise is resolved.

  }).then( function() {                    // this fires after the asyncWithPromise
                                           // above resolves.
    doSomethingElseSynchronous();
    return someOtherValue;
  });;
            

How to use promises in your own code

How return values propagate a chain of `.then()`'s

If return value is a promise, its 'success' value is passed to the next `then()`

If return value is a non-promise, that value is given to the next `then()`


  asyncWithPromise().then( function(successValue){  // successValue is whatever
                                                    // asyncWithPromise `resolve()`'d with
    return true;
  }).then( function(successValue) {                 // successValue is true
    return anotherAsyncWithPromise();
  }).then( function(successValue) {                 // successValue is whatever
                                                    // anotherAsyncWithPromise
                                                    // `resolve()`'d with
    return true;
  });;
            

Wrapping non-promise code with rsvp-tools

https://github.com/mixonic/rsvp-tools is a library we created with a set of wrappers to:

  • turn callback-based APIs into promise-based
  • turn event-based APIs into promise-based

Thank you

You can view this talk online at:
http://bit.ly/utahjs-promises