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 resultcallback(null, result)elsecallback(new Error('No data!'))
Previous example
as theoretical synchronous code
App.getData = (url) ->response = http.request(url) # blockif result = response.getData() # blockreturn resultelsethrow new Error('No data!')
Two examples side by side. What's new? What's missing?App.getData = (url, callback) ->http.request url, (response) ->response.on 'data', (result) ->if resultcallback(null, result)elsecallback(new Error('No data!'))
App.getData = (url) ->response = http.request(url) # blockif result = response.getData() # blockreturn resultelsethrow 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 resultelsethrow new Error('No data!')
In callback style, there is
NO THROW
App.getData = (url, callback) ->http.request url, (response) ->response.on 'data', (result) ->if resultreturn resultelse# 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?
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 errorhandleError(error)elserender(user)
Call then on returned Promise
getUser('john').then(render, handleError)
Rule #2
Instead of calling a callback
getUser = (username, callback) ->$.ajax url: '/' + usernamesuccess: (data) -> callback(null, data)error: (reason) -> callback(reason)
Return a Promise
getUser = (username, callback) ->new Promise (resolve, reject) ->$.ajax url: '/' + usernamesuccess: resolveerror: reject
Transforms
between synchronous and promise world
Case #1
Blocking functions in sequence
user = getUser('john')name = getData(user)feed = query(data)
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
getUser('john').then (user) ->throw new Error('No user!') if not user
Case #3
Catching exceptions
tryuser = getUser('john')catch errorhandleError(error)
getUser('john').then (user) -> user.name.then undefined, (error) -> # onFulfilled = undefinedhandleError(error)
Case #4
Re-throwing exceptions
tryuser = getUser('john')throw new Error('No user!') if not usercatch errorthrow new Error('Error: ' + error)
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 errorhandleError(error)elsegetFeed user, (error, feed) ->if errorhandleError(error)elsecallback(feed)
getUser('john').then (user) -> getFeed(name).then (feed) -> callback(feed).then undefined, handleError
Converting Promise to a
Callback-passing style function
Usage:callbackMeUser = (user, callback) ->getUser(user).nodeify(callback)
callbackMeUser 'john', (error, user) ->if errorhandleError(error)elsesayHi(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.all:promises = [getUser('alice'), getUser('bob')]Q.spread promises, (alice, bob) ->engage(alice, bob)
promises = [getUser('alice'), getUser('bob')]Q.all promises, (users) -> # passes arrayengage(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

Web Developer at Monterail.com
Promises/A+
By Adam Stankiewicz
Promises/A+
- 1,294


