Promises

an introduction

Stuart Knightley
@stuk
github.com/Stuk

Me

  • Been working in Javascript on and off for 10 years
  • Using promises for about a year
  • Currently working on the Montage HTML5 framework (where we use promises extensively)
    • Mr – CommonJS modules in the browser
    • Mop – Optimised CommonJS modules in the browser

Asynchronous Javascript

+ first class functions

= callbacks

Let's look at some examples

Sequential async

One async thing after another

browser.get("http://admc.io/wd/test-pages/guinea-pig.html",
function() {
  browser.title(function(err, title) {
    assert.ok(~title.indexOf('I am a page title - Sauce Labs'),
        'Wrong title!');
    browser.elementById('i am a link', function(err, el) {
      browser.clickElement(el, function() {
        browser.eval("window.location.href", function(err, href) {
          assert.ok(~href.indexOf('guinea-pig2'));
          browser.quit();
        });
      });
    });
  });
});
        
Example from wd.js readme

Name the functions

browser.get("http://admc.io/wd/test-pages/guinea-pig.html",
  pageLoaded);
function pageLoaded() {
  browser.title(checkTitle);
}
function checkTitle(err, title) {
  assert.ok(~title.indexOf('I am a page title - Sauce Labs'), 'Wrong title!');
  browser.elementById('i am a link'), clickElement);
}
function clickElement(err, el) {
  browser.clickElement(el, getHref);
}
function getHref() {
  browser.eval("window.location.href", checkHref);
}
function checkHref(err, href) {
  assert.ok(~href.indexOf('guinea-pig2'));
  browser.quit();
}

Parallel async

Do something when several tasks have completed

Easiest

function comparePaths(a, b, cb) {
    fs.readFile(a, function (err, aData) {
        if (err) return cb(err);
        fs.readFile(b, function (err, bData) {
            if (err) return cb(err);
            compareData(aData, bData, cb);
        });
    });
};
        

But not parallel!

Flags

function comparePaths(a, b, cb) {
    var aData, bData, error;
    function done() {
        if (!error && aData && bData) compareData(aData, bData, cb);
    }
    fs.readFile(a, function (err, data) {
        if (err && !error) return cb(error = err);
        aData = data
        done();
    });
    fs.readFile(b, function (err, data) {
        if (err && !error) return cb(error = err);
        bData = data
        done();
    });
};
        

Boring (and tricky) code to write

Exxxx xxxxxxxg

Quiz time

Spot the bug

function readJSON(fileName, callback) {
    fs.readFile(fileName, "utf8", function (err, data) {
        if (err) {
            callback(err);
            return;
        }
        var object = JSON.parse(data);
        callback(null, object);
    })
}

Synchronous errors don't get caught

readJSON("bad.json", function (err, object) {
    if (err) return console.log("Oops");
    console.log(object);
});

Expected

Oops

Actual

SyntaxError: Unexpected end of input
    at Object.parse (native)
    at /Users/stuart/.../read-json.js:9:27
    at fs.readFile (fs.js:176:14)
    at Object.oncomplete (fs.js:297:15)

try ... catch

function readJSON(fileName, callback) {
    fs.readFile(fileName, "utf8", function (err, data) {
        if (err) {
            callback(err);
            return;
        }
        var object;
        try {
            object = JSON.parse(data);
        } catch (e) {
            return callback(e);
        }
        callback(null, object);
    })
}

Let's guess what I'm going to suggest as the solution

Yep, it's promises

An analogy

Getting values out

.then(function (value) {
    // success (resolved)
}, function (error) {
    // failure (rejected)
});

.then returns another promise

var nextPromise = .then(function (value) {
    return 2;
})

nextPromise.then(function (value) {
    assert(value === 2);
});

.then returns another promise

.then(function (value) {
    return 2;
}).then(function (value) {
    assert(value === 2);
});

The return can be a promise as well

.then(function (value) {
    return readFile("a.txt");
}).then(function (value) {
    assert(value === /* the contents of a.txt */ );
});

Let's look at the examples again

Sequential async

browser.get("http://admc.io/wd/test-pages/guinea-pig.html")
.then(function () {
    return browser.title();
}).then(function (title) {
    assert.ok(~title.indexOf('I am a page title - Sauce Labs'),
        'Wrong title!');
    return browser.elementById('i am a link');
}).then(function (el) {
    return browser.clickElement(el);
}).then(function () {
    return browser.eval("window.location.href");
}).then(function (href) {
    assert.ok(~href.indexOf('guinea-pig2'));
}).finally(function () {
    browser.quit();
});

Parallel async

var readFile = Q.denodeify(fs.readFile);

function comparePaths(a, b) {
    return Q.all([readFile(a), readFile(b)])
    .then(function (data) {
        return compareData(data[0], data[1]);
    });
}

Error handling

var readFile = Q.denodeify(fs.readFile);

function readJSON(fileName) {
    return readFile(fileName, "utf8")
    .then(function (data) {
        var object = JSON.parse(data);
        return object;
    });
}

Usage

readJSON("bad.json")
.then(function (object) {
    console.log(object);
}, function (err) {
    console.log("Oops");
});

Cohabiting with callbacks

wd.js (webdriver)

Control a real web browser with Javascript

You can use promises and callbacks at the same time

var wd, browser, promise;
wd = require("wd");
browser = wd.promiseRemote();

promise = browser.init({ browserName: 'firefox' },
function (err, sessionId) {
    // callback
    console.log(sessionId);
});

promise.then(function (sessionId) {
    // promise
    console.log(sessionId)
}).done();
        

Converting sounds like a lot of work

Promise Me

http://stuk.github.io/promise-me/

"parses your code and then manipulates the AST to transform the callbacks into calls to then()"

browser.init({ },
function (err, sessionId) {
    // callback
    console.log(sessionId);
});
browser.init({ }).then(function (sessionId) {
    // callback
    console.log(sessionId);
});
browser.init({ }, function (err, sessionId) {
    if (err) {
        console.error(err);
        return;
    }
    console.log(sessionId);
});
browser.init({}).then(function (sessionId) {
    console.log(sessionId);
}, function (err) {
    console.error(err);
    return;
});
browser.init({ }, function (err, sessionId) {
    console.log(sessionId);
    browser.eval("return document.title", function (err, title) {
        console.log(title);
    });
});
browser.init({}).then(function (sessionId) {
    console.log(sessionId);
    return browser.eval("return document.title");
}).then(function (title) {
    console.log(title);
});
browser.init({ }, function (err, sessionId) {
    console.log(sessionId);
    browser.eval("return document.title", function (err, title) {
        console.log(sessionId, title);
    });
});
browser.init({}).then(function (sessionId) {
    console.log(sessionId);
    browser.eval("return document.title").then(function (title) {
        console.log(sessionId, title);
    });
});

Promises

  • make the results of async operations first class values
  • handle errors well
  • can work in tandem with callbacks

Thanks for listening

Q promise library

@stuk
github.com/Stuk

Box image from the Gnome project