Promises/A+



Note:

When I write callback style I mean
Continuation-passing style
(error and value as callback parameters)
getUser 'john', (error, user) ->  // ...  getFeed user, (error, feed) ->    // ...

Callback

A callback is a piece of executable code that is passed as an argument to other code, which is expected to call back (execute) the argument at some convenient time.
Wikipedia, The Free Encyclopedia.
App.getData = (url, callback) ->
http.request url, (response) -> response.on 'data', (result) -> if result callback(null, result) else callback(new Error('No data!'))

Previous example 

as theoretical synchronous code

App.getData = (url) ->
response = http.request(url) # block if result = response.getData() # block return result else throw new Error('No data!')
App.getData = (url, callback) ->
http.request url, (response) -> response.on 'data', (result) -> if result callback(null, result) else callback(new Error('No data!'))
Two examples side by side. What's new? What's missing?
App.getData = (url) ->
response = http.request(url) # block if result = response.getData() # block return result else throw new Error('No data!')

In callback style, there is

NO RETURN

App.getData = (url, callback) ->
http.request url, (response) -> response.on 'data', (result) -> if result # Nobody will receive it! return result else throw new Error('No data!')

In callback style, there is

NO THROW

App.getData = (url, callback) ->
http.request url, (response) -> response.on 'data', (result) -> if result return result else # Nobody will catch it! throw new Error('No data!')


From official docs. Crashes whole application.
fs.rename "/hello", "/world", (err) ->
  if err
    throw err # LOL
  else
    fs.stat "/world", (err, stats) ->
      if err
        throw err # LOL2
      else
        console.log "stats: " + stats

Previous solution: allow for one more callback
process.on 'uncaughtException', (err) ->
// Any last words? console.log('Exception: ' + err)
fs.rename "/hello", "/world", (err) -> if err throw err else fs.stat "/world", (err, stats) -> if err throw err else console.log "stats: " + stats

Current "solution". Crash one worker at the time.
fork = domain.create()fork.on 'error', (err) ->  console.log('Exception: ' + err)fork.run ->   fs.rename "/hello", "/world", (err) ->
    if err
      throw err 
    else
      fs.stat "/world", (err, stats) ->
        if err
          throw err
        else
          console.log "stats: " + stats

In callback style, there are

NO GUARANTEES

  • callbacks can be called twice!
    • Old Express.js bug
      (connected with throwing Exceptions)
  • callbacks create side efects!
    • Called callback can produce side effect
      in unpredicatable way.

We quickly descent into

callback hell

var p_client = new Db('integration_tests_20');
p_client.open(function(err, p_client) {
    p_client.dropDatabase(function(err, done) {
        p_client.createCollection(function(err, collection) {
            collection.insert({'a':1}, function(err, docs) {
                collection.find({'_id': 12}, function(err, cursor) {
                    cursor.toArray(function(err, items) {
                        test.assertEquals(1, items.length);
                        p_client.close();
                    });
                });
            });
        });
    });
});
(Not as much, but you get the idea. Plus error handling)

All to all

  • NO STACK (return/catch)

  • NO GUARANTEES

  • CALLBACK HELL

  • BUT WE DON'T BLOCK!
    is it worth it?

Promise is an object
behaving in certain way.


Promise/A+ is a standard
how exactly it should behave.

Promise

  • was discovered circa 1989
  • widely used outside JS
  • deals with callback hell (chaining callbacks)
  • re-introduces stack (return/throw)
  • provides guarantees about async code

Promises are objects

that represent a value 

that may not be available yet

promise = getUser('john')promise.then (user) ->  user.sayHi()

promise.then (onFulfilled, onError)

the only official API of the Promise/A+ object™

// then :: Promise a -> (a -> b) -> Promise b
// then :: Promise a -> (a -> Promise b) -> Promise b
                      |
                      v
               +-------------+
               |             |
       +-------+   Pending   +------+
       |       |             |      |
       |       +-------------+      |
       |                            |
       | Promise is a state machine |
       v                            v
+-------------+             +--------------+
|             |             |              |
+  Fulfilled  +             +   Rejected   +
|             |             |              |
+-------------+             +--------------+


Rule #1

Instead of  passing a callback

getUser 'john', (error, user) ->  if error    handleError(error)  else    render(user)

Call then on returned Promise

getUser('john')  .then(render, handleError)

Rule #2

Instead of calling a callback

getUser = (username, callback) ->  $.ajax url: '/' + username    success: (data) -> callback(null, data)    error: (reason) -> callback(reason)

Return a Promise

getUser = (username, callback) ->
  new Promise (resolve, reject) ->    $.ajax url: '/' + username      success: resolve      error: reject

Transforms

between synchronous and promise world

Case #1

Blocking functions in sequence

user = getUser('john')name = getData(user)feed = query(data)
becomes
getUser('john')  .then (user) -> getData(user)  .then (data) -> query(data)

Case #2

Throwing exceptions

user = getUser('john')throw new Error('No user!') if not user
becomes
getUser('john')  .then (user) ->    throw new Error('No user!') if not user

Case #3

Catching exceptions

try  user = getUser('john')catch error  handleError(error)
becomes
getUser('john')  .then (user) -> user.name  .then undefined, (error) -> # onFulfilled = undefined    handleError(error)

Case #4

Re-throwing exceptions

try  user = getUser('john')  throw new Error('No user!') if not user
catch error throw new Error('Error: ' + error)
becomes
getUser('john')  .then (user) -> user.name  .then undefined, (error) ->    throw new Error('Error: ' + error)

Transforms

between callback-passing style and Future world

Error handling

getUser 'john', (error, user) ->  if error    handleError(error)  else    getFeed user, (error, feed) ->      if error        handleError(error)      else        callback(feed)
becomes
getUser('john')  .then (user) -> getFeed(name)  .then (feed) -> callback(feed)  .then undefined, handleError

Converting Promise to a 

Callback-passing style function

callbackMeUser = (user, callback) ->
getUser(user).nodeify(callback)
Usage:
callbackMeUser 'john', (error, user) ->
if error handleError(error)  else sayHi(user)

Converting to a Promise style

getUser = callbackMeUser.denodeify()
Usage:
getUser('john')
.then (user) ->  sayHi(user) .then undefined, (error) -> handleError(error)

Running Promises in parallel

Callback is called only if all promises are fulfilled.

Q.spread:
promises = [getUser('alice'), getUser('bob')]Q.spread promises, (alice, bob) ->  engage(alice, bob)
Q.all:
promises = [getUser('alice'), getUser('bob')]Q.all promises, (users) -> # passes array  engage(users[0], users[1])

Introducing

Promises/A+

Executable specification!


Promises/A+ does not define:

  • How to instantiate Promise/A+ object
  • How to implement Promise/A+
  • Additional API on top of the basic one (then method)
  • Anything about onProgress callback

28 compatible implementations

you can use any of them, they are all compatible


jQuery is not one of them :(


It  has some pitfalls:

  • before 1.8: then method doesn't even return a promise
  • after 1.8: doesn't handle well edge cases (e.g. throw)

My humble recommendations

  • then/promise - bare bones API, few addons
  • RSVP - little more advanced, simple
  • Q - powerful, battle-tested, but strange API
  • when - powerful, nice and huge API

https://github.com/promises-aplus/promises-spec/blob/master/implementations.md 

Promise/A

Promise/A+

Both are standards.

What is the difference?

Promise/A+

Clarify some edge cases

What if onFulfilled or onRejected returns a promise?


Answer:
Promise returned by then "becomes" that one
promise = Application.getUser()  .then (user) ->    return Twitter.fetchUserTweets()promise.then (userTweets) ->
display(userTweets)
// then :: Promise a -> (a -> b) -> Promise b 
// then :: Promise a -> (a -> Promise b) -> Promise b

What if onFulfilled or onRejected returns a promise?


Answer:
Promise returned by then "becomes" that one
Application.getUser()  .then -> (user)    return Twitter.fetchUserTweets(user)  .then (userTweets) ->     # display them?
// then :: Promise a -> (a -> b) -> Promise b 
// then :: Promise a -> (a -> Promise b) -> Promise b

What if onRejected 
throws an exception?


Answer:
Exception is passed to the next onRejected handler
App.bogusGetUser()  .then(getTweets, (e) -> conle.log(e))  .then(display, (e) -> console.log(e))
# => ReferenceError {}

What if promise called upon
is
already fulfilled or rejected?

Answer?

onFulfilled and onRejected
should be called asynchronously


# Won't block:promise.then blockForever

And other interesting only 

 for library developers

- Omissions (free hand)
- Clarifications (bad implementations)
- Additions (most already covered)
- yada yada yada

Thank you 

Adam Stankiewicz (sheerun)
Web Developer at Monterail.com

Promises/A+

By Adam Stankiewicz