Promises/A+

Async flow control without callback hell

Mikael Karon <mikael.karon@ef.com>
EF Labs (Shanghai)

Ancient history

The term promise was proposed in 1976 by Daniel P. Friedman and David Wise, and Peter Hibbard called it eventual.

A somewhat similar concept future was introduced in 1977 in a paper by Henry Baker and Carl Hewitt.

The term promise was coined by arbara Liskov and Liuba Shrira in 1988

The future and/or promise constructs were first implemented in programming languages such as MultiLisp and Act 1. ca. 1989.

recent history

2008.02.15
The first widespread use of this pattern was with the dojo toolkit deferred object in version 0.9.
2009.03.24
Common JS Promises API proposal by Kris Zyp
2011.12.31
jQuery introduced a new concept in version 1.5 called Deferred which is also a derivative implementation of the CommonJS Promises/A proposal.

But $.Deferred sucked

  • Different from dojo, jQuery does not return another promise from the then method. Instead, jQuery provides the pipe method to chain operations together
  • Misses the sync - async parallel
  • Does not handle errors (throw)
  • Not interoperable with other 'thenable'
  • Only from 1.8 support returning a promise

... so Domenic Denicola got angry and wrote

...and he made http://promisesaplus.com/

Standards === win

  • >20 implementations of promises based on Promise/A+
  • version 1.1 of spec is almost ready
  • several other siblings spec are under development (promise creation, cancellation, progress)
  • DOMFuture promise library for using promises in DOM API

What is "callback hell"?

Asynchronous JavaScript, or JavaScript that uses callbacks, is hard to get right intuitively.

A short example

fs.readFile('test.txt', function (err, data) {
    if (err) return console.error(err);
    db.insert({
        fileName: 'test.txt',
        fileContent: data
    }, function (err, result) {
        if (err) return console.error(err);
        smtp.send({
            to: 'test@test.com',
            body: 'This is a test.'
        }, function (err) {
            if (err) return console.error(err);
            console.log("Success email sent.");
        });
    });
});
See all the instances of function and })? Eek! This is affectionately known as callback hell

Fix: Name your functions

fs.readFile('test.txt', function onReadFile(err, data) {
    db.insert(..., function onInsert(err, result) {
        smtp.send(..., function onSend(err) { ... });
    });
});

As you can see naming functions is super easy and does some nice things to your code:

  • Makes code easier to read
  • When exceptions happen you will get stacktraces that reference actual function names instead of "anonymous"
  • Allows you to keep your code shallow, or not nested deeply, which brings me to my next point:

Fix: Keep your code shallow

fs.readFile('test.txt', onReadFile);
function onReadFile(err, data) {
    if (err) return console.error(err);
    db.insert({
        fileName: 'test.txt',
        fileContent: data
    }, onInsert);
}
function onInsert(err, result) {
    if (err) return console.error(err);
    smtp.send({
        to: 'test@test.com',
        body: 'This is a test.'
    }, onSend);
}
function onSend(err) {
    if (err) return console.error(err);
    console.log("Success email sent.");
}

Promises in JavaScript

A Promise represents the pending result of a computation that may not have completed yet.

The states of a promise

  • Unresolved
  • Fulfilled
  • Rejected
Once in the fulfilled or rejected state, a Promise become immutable--neither its state nor its result (or error/reason) can be modified.

Unresolved

A Promise starts in an unresolved or pending state. For example, the Promise for a computation that hasn't yet completed is in the pending state. At some point the computation will either complete successfully, thus producing a result, or fail, either generating some sort of error or reason why it could not complete.

Fulfilled

If the computation completes successfully, its Promise will transition to the fulfilled state, and all consumers will be notified of the actual result. In other words, their callback will be invoked and passed the result.

Rejected

If the computation fails, its Promise will transition to the rejected state, and all consumers will be notified of the error or reason for the failure. In other words, their errorback will be invoked and passed the result.

Promise me this

fs.readFile('test.txt').then(function (data) {
    return db.insert({
        fileName: 'test.txt',
        fileContent: data
    });
}).then(function (result) {
    return smtp.send({
        to: 'test@test.com',
        body: 'This is a test.'
    });
}).then(function () {
    console.log("Success email sent.");
}, console.error);
  • read 'test.txt'
  • then write contents to database
  • then send a mail
  • then console log a message
  • if any of the above operations fail, console.error

Can I haz more?

fs.readFile('test.txt').then(function (data) {
    return db.insert({
        fileName: 'test.txt',
        fileContent: data
    });
}).then(function (result) {
    return smtp.send({
        to: 'test@test.com',
        body: 'This is a test.'
    });
}).then(function () {
    console.log("Success email sent.");
}).then(function () {
    return ajax.request({ url: "/some/path" });
}).then(console.info, console.error, console.debug);
  • read, insert, send, log, ajax
  • then console.info, console.error, console.debug

Deferred producers and consumers

A producer is responsible for providing the result of the computation, and typically, but not always, will be the same component that created the Deferred.

As the name implies, a consumer observes the result of the computation.

From the outside consumers can observe the outcome the computation, but not interfere with the computation itself.

From the inside the the producer can resolve the promise with the outcome from the computation.

Advanced promises

*Your mileage may vary depending on implementation

  • forwarding
  • .join and .all
  • .any and .some
  • .map and .reduce
  • .sequence and .pipeline
  • .parallel

Questions?

Promises/A+

By Mikael Karon