Promises

This is a set of experiments with the Promise pattern using RSVP.js.

I recently gave a talk about Promises at a UtahJS meetup. These are some of the examples from that talk, cleaned up and with explanations. Each one is runnable.

I am Cory Forsyth, on twitter as @bantic.

(See also my blog post for more.)

Nested without promises

Here we use a simple `async` function that takes a callback and executes it later. In order to have the code execute in order we have to nest our callbacks.

function async(msg, callback){
  setTimeout( function(){
    log('async: ' + msg);
    callback();
  }, 25);
}

function sync(msg){
  log('sync: ' + msg);
}

function nestedWithoutPromises(){
  async(1, function() {
    sync(1);
    async(2, function(){
      sync(2);
    });
  });
}

Nested using promises

This time, we return a promise from our async callback that resolves after a timeout.

var asyncPromise = function(msg){
  return new RSVP.Promise( function(resolve, reject){
    setTimeout( function(){
      log('asyncPromise: ' + msg);
      resolve();
    }, 25);
  });
};

function nestedWithPromises(){
  asyncPromise(1)
  .then( function(){
    sync(1);
  })
  .then( function(){
    return asyncPromise(2);
  })
  .then( function(){
    sync(2);
  });
}

Quiz: Promise Gotcha

This example is similar to the previous one, with an important difference. Before clicking "Run it", look carefully and take a guess at what you think the output will be.

Why does 'sync: 2' log before 'asyncPromise: 2' does? If you compare with the example above, you'll see that in this case we are not returning a value from the `then` which calls `asyncPromise(2)`. The return value of that `then` will thus be `undefined`. The Promises/A+ spec specifies that if a `then` returns a non-promise value, the chained promise will be resolved with that value. In this case, the `then` chained to the `asyncPromise(2)` call will fire without waiting for the `asyncPromise(2)` call to be resolved. Since it takes a little time for the async promise to log but no time for the sync call to log, we see the log output of the sync first.

The lesson here is, if you want to chain promises, you must explicitly return a promise from a fulfillment handler.

function promiseGotcha(){ // don't use this code!
  asyncPromise(1)
  .then( function(){
    sync(1);
  })
  .then( function(){
    asyncPromise(2);
  })
  .then( function(){
    sync(2);
  });
}
        

Rejected Promises

A promise can be rejected instead of being fulfilled. In this case, the second argument to the `then` method will be called. Here's an example.

var rejectedPromise = function(message){
  return new RSVP.Promise(function(resolve, reject){
    setTimeout(function(){
      log('asyncPromise: ' + message + '(rejected)');
      reject();
    }, 25);
  });
};

function rejectedPromises(){
  rejectedPromise(1)
  .then( function(){
    sync('will not be called');
  }, function(){
    sync('Rejected!');
  });
}
        

Exceptions in Promise Fulfillment Handlers

What happens if the `onFulfilled` handler raises an exception?

function promiseWithFailedFulfillment(){
  asyncPromise(1)
  .then( function(){
    throw new Error('oops');
  }, function(){
    sync('first onRejected handler');
  })
  .then( function(){
    sync('second onFulfilled handler');
  }, function(){
    sync('second onRejected handler');
  });
}
        

Exceptions in Promise Rejection Handlers

What happens if the `onRejected` handler raises an exception?

Answer: The next chained rejection handler is also called.

function promiseWithFailedRejection(){
  rejectedPromise(1)
  .then( function(){
    sync('first onFulfilled handler (not called)');
  }, function(){
    sync('first onRejected handler');
    throw new Error('oops');
  })
  .then( function(){
    sync('second onFulfilled handler');
  }, function(){
    sync('second onRejected handler');
  });
}
        

Resolve and Reject a Promise

What happens if we reject a promise after it has been resolved?

Answer: The promises/A+ spec tells us that, when either fulfilled or rejected once, a promise must not transition into any other state. So the first transition (in this case, to fulfilled) is the one that is applied.

var resolveRejectPromise = function(){
  return new RSVP.Promise(function(resolve, reject){
    setTimeout(function(){
      log('asyncPromise resolving');
      resolve();
      log('asyncPromise rejecting');
      reject();
    }, 25);
  });
};

function resolveRejectPromiseChain(){
  resolveRejectPromise()
  .then( function(){
    sync('onFulfilled');
  }, function(reason){
    sync('onRejected');
  });
}
        

For Fun: Heisenpromise

Here's a function that returns a promise that will be rejected half the time. Try running it a couple different times. Before you click: Is it possible to get to the 'Ultimately Succeeded' line, and if so, in how many different ways?

var heisenPromise = function(msg){
  return new RSVP.Promise(function(resolve, reject){
    setTimeout( function(){
      if (Math.random() > 0.5) {
        log(msg + ' (succeeded)');
        resolve();
      } else {
        log(msg + ' (failed)');
        reject();
      }
    }, 25);
  });
};

function heisenPromiseChain(){
  heisenPromise(1)
  .then( function(){
    return heisenPromise('2a');
  }, function(){
    return heisenPromise('2b');
  })
  .then( function(){
    sync('Ultimately Succeeded!');
  }, function(){
    sync('Ultimately failed.');
  });
}
        

Passing values using promises

When an onFulfilled handler returns a value, that value is passed to the next chained `then`. If onFulfilled handler returns a promise, then the resolved value of that promise is passed.

var asyncPromise = function(msg, returnValue){
  return new RSVP.Promise(function(resolve, reject){
    setTimeout( function(){
      log('asyncPromise: ' + msg + ' (returning: ' + returnValue + ')');
      resolve(returnValue);
    }, 25);
  });
};

function promiseChainValues(){
  asyncPromise(1, 'foo')
  .then( function(val) {
    sync('fulfilled with: ' + val);
  });
}
        

Passing values from rejections

Same thing happens when we `reject` with a value as when we `resolve` with a value, except the value is passed into the onRejected handler

var rejectedPromise = function(message, reason){
  return new RSVP.Promise(function(resolve, reject){
    setTimeout(function(){
      log('asyncPromise: ' + message + ' (rejected: ' + reason + ')');
      reject(reason);
    });
  });
};

function promiseChainRejectedReasons(){
  rejectedPromise(1, 'bar')
  .then( function(){
    sync('onFulfilled handler does not fire');
  }, function(reason){
    sync('onRejected handler called with reason: ' + reason);
  });
}