Using Libraries and Frameworks - JavaScript with Promises (2015)

JavaScript with Promises (2015)

Chapter 4. Using Libraries and Frameworks

Before the ES6 Promise API existed, many JavaScript libraries and frameworks implemented their own version of Promises. Some libraries were written for the sole purpose of providing promises while established libraries like jQuery added them to handle their async APIs.

Promise libraries can act as polyfills in older web browsers and other environments where native promises are not provided. They can also supplement the standard API with a wide set of functions for managing promises. If your code only uses promises that you create you’re in a good position to choose a library and take full advantage of its extended API. And if you are handling promises that other libraries produced, you can wrap those promises with ones from your chosen library to access the additional features.

This chapter focuses on nonnative promise implementations. The majority of the chapter covers Bluebird, a fast and robust promise library. Although Bluebird is a compelling choice, there are other good options. For example, the Q promise library predates Bluebird and is widely used in applications and frameworks including AngularJS. Q and other libraries are not discussed in detail because this chapter is not a guide to choosing between libraries. It is an introduction to the enhancements that third-party libraries offer to demonstrate their value. The Promise implementation in jQuery is also discussed because of jQuery’s immense popularity. However, this is not a complete walk-through of either Bluebird or jQuery. These open source projects evolve rapidly, so refer to the official online documentation for full details of the current features.

Promise Interoperability and Thenables

Before diving into the details of specific libraries, let’s discuss how promises from different libraries can be used with one another. The basis of all interoperability between promise implementations is the thenable contract. Any object with a then(onFulfilled, onRejected) method can be wrapped by any standard promise implementation.

As Kris Kowal wrote when reviewing this chapter, “…regardless of what that method returns, regardless of what onFulfilled and onRejected return, and in fact regardless of whether onFulfilled or onRejected are executed synchronously or asynchronously, [then] is sufficient for any of these Promise implementations to coerce the thenable into a well-behaved, always-asynchronous, always returning capital-P Promise, promise. This is particularly important when consuming promises from unreliable third parties, where unreliable can be as innocuous as a backward-incompatible version of the same library.”

Example 4-1 shows an example of a simple thenable object wrapped with a standard promise.

Example 4-1. Wrapping a thenable for interoperability

function thenable(value) {

return {

then: function (onfulfill, onreject) {

onfulfill(value);

}

};

}

var promise = Promise.resolve(thenable('voila!'));

promise.then(function(result) {

console.log(result);

});

// Console output:

// voila!

Although it is unlikely you will encounter such a sparse thenable in your own code, the same concept applies to wrapping promises from other implementations to work as the promise implementation that your code prefers.

The Bluebird Promise Library

Bluebird is an open source promise library with a rich API and excellent performance. The Bluebird GitHub repo includes benchmarks that show it outperforming other implementations, including the native version in the V8 JavaScript engine used by Node.js and Google Chrome. Bluebird’s author Petka Antonov says native implementations are more focused on matching behavior specifications than performance optimization, which allows carefully tuned JavaScript to outperform native code.

Bluebird offers many other features including elegant ways of managing execution context, wrapping Node.js APIs, working with collections of promises, and manipulating fulfillment values.

Loading Bluebird

When Bluebird is included in a web page using a script tag, it overwrites the global Promise object by default with its own version of Promise. Bluebird can also be loaded in the browser in other ways, such as an AMD module using require.js, and it is available as an npm package for use in Node.js.

The Bluebird Promise object can serve as a drop-in replacement or polyfill for the ES6 Promise. When the Bluebird script is loaded in a web browser it overwrites the global Promise object. However, you can use Promise.noConflict() after loading Bluebird to restore the global Promise object to its previous reference in order to run Bluebird side by side with native promises. As explained in the earlier section on interoperability and thenables, you can treat other promise implementations as Bluebird promises by wrapping them using [Bluebird Promise].resolve(promise). In Example 4-2, Bluebird wraps a native promise to expose functions that reveal its state.

Example 4-2. Wrap a native promise with a Bluebird promise

// Assume bluebird has been loaded using <script src="bluebird.js"></script>

var Bluebird = Promise.noConflict(); // Restore previous reference to Promise

var nativePromise = Promise.resolve(); // Native Promise

var b = Bluebird.resolve(nativePromise); // Wrap native promise with Bluebird promise

// Force event loop to turn

setTimeout(function () {

console.log('Pending? ' + b.isPending()); // Pending? false

console.log('Fulfilled? ' + b.isFulfilled()); // Fulfilled? true

console.log('Rejected? ' + b.isRejected()); // Rejected? false

}, 0);

The remaining examples in this chapter that relate to Bluebird assume that all promises are Bluebird promises.

Managing Execution Context

Callbacks frequently need access to variables in their enclosing scope. Two common ways of accessing those variables are shown in the configure and print methods in Example 4-3. Both access the pageSize property of a printer object.

Example 4-3. Using the enclosing scope through function.bind() or aliasing

var printer = {

pageSize: 'US LETTER',

connect: function () {

// Return a promise that is fulfilled when a connection

// to the printer is established

},

configure: function (pageSize) {

return this.connect().then(function () {

console.log('Setting page size to ' + pageSize);

this.pageSize = pageSize;

}.bind(this)); // Using bind to set the context

},

print: function (job) {

// Aliasing the outer context

// _this, that, and self are some other common alias names

var me = this;

return this.connect().then(function () {

console.log('Printing job using page size ' + me.pageSize);

});

}

};

printer.configure('A4').then(function () {

return printer.print('Test page');

});

// Console output:

// Setting page size to A4

// Printing job using page size A4

The configure method uses bind(this) to share its context with the inner callback. The print method aliases the outer context to a variable called me in order to access it inside the callback.

Bluebird offers an alternative way of exposing the enclosing scope by adding a promise.bind() method that sets the context for all subsequent callbacks used in a promise chain, as shown in Example 4-4.

Example 4-4. Setting callback contexts using promise.bind()

printer.shutdown = function () {

this.connect().bind(this).then(function() { // bluebird.bind not function.bind

console.log('First callback can use ' + this.paperSize);

}).then(function () {

console.log('And second callback can use ' + this.paperSize);

});

};

// Console.output:

// First callback can use A4

// And second callback can use A4

Using bluebirdPromise.bind() has an advantage over the previous two solutions because it removes the call to bind for individual functions in a long chain and avoids adding a reference to the enclosing scope of each callback.

The effect of bind applies to all subsequently chained promises, even those on a promise that a function returns. To avoid leaking objects used as the context in bind, you can mask the effect by calling bind again before returning a bound promise chain from a function. This practice is even more important if the function is being consumed as a third-party library.

Example 4-5 shows an updated version of the printer.shutdown() method that masks the printer context that the callbacks inside it use.

Example 4-5. Hiding the bound context from calling code

printer.shutdown = function () {

return this.connect().bind(this).then(function () {

//...

}).then(function () {

//...

}).bind(null); // mask the previous binding

};

printer.shutdown().then(function () {

console.log('Not running in the context of the printer: ' + this !== printer);

});

// Console.output:

// This code is not running in the context of the printer: true

Wrapping Node.js Functions

Node.js has a standard way of using callbacks in async functions. The node-style expects a callback as the last argument of a function. The first parameter of the callback is an error object followed by any additional parameters. Example 4-6 shows a version of the loadImage function implemented in this style.

Example 4-6. Node-style callback

function loadImageNodeStyle(url, callback) {

var image = new Image();

image.src = url;

image.onload = function () {

callback(null, image);

};

image.onerror = function (error) {

callback(error);

};

}

loadImageNodeStyle('labyrinth_puzzle.png', function (err, image) {

if (err) {

console.log('Unable to load image');

return;

}

console.log('Image loaded');

});

Bluebird provides a convenient function named promisify that wraps node-style functions with ones that return a promise, as shown in Example 4-7.

Example 4-7. Using promisify to wrap a node-style function

var loadImageWrapper = Bluebird.promisify(loadImageNodeStyle);

var promise = loadImageWrapper('computer_problems.png');

promise.then(function (image) {

console.log('Image loaded');

}).catch(function (error) {

console.log('Unable to load image');

});

The loadImageWrapper function accepts the same url argument as the original loadImageNodeStyle function but does not require a callback. Using promisify creates a callback internally and correctly wires it to a promise. If the callback receives an error the promise is rejected. Otherwise the promise is fulfilled with any additional arguments passed to the callback.

Standard promises cannot be fulfilled by more than one value. However, some node-style callbacks expect more than one value when an operation succeeds. In this case you can instruct promisify to fulfill the promise with an array containing all the arguments passed to the function except the error argument, which is not relevant. The array can be converted back to individual function arguments using Bluebird’s promise.spread() method. Example 4-8 shows an example of a node-style function that provides multiple pieces of information about a user’s account.

Example 4-8. Converting arrays into individual arguments using promise.spread()

function getAccountStatus(callback) {

var error = null;

var enabled = true;

var lastLogin = new Date();

callback(error, enabled, lastLogin); // Callback has multiple values on success

}

var fulfillUsingAnArray = true;

var wrapperFunc = Bluebird.promisify(getAccountStatus, fulfillUsingAnArray);

// Without using spread

wrapperFunc().then(function (status) {

var enabled = status[0];

var lastLogin = status[1];

// ...

});

// Using spread

wrapperFunc().spread(function (enabled, lastLogin) {

// ...

});

Using spread in this example allows the enabled and lastLogin values to be clearly specified without the need to extract them from an array. Use spread to simplify the code whenever a promise is fulfilled with an array whose length and order of elements are known.

ES6 includes a feature called destructuring that can assign values from an array to individual variables. This feature is described in “Destructuring”.

If you want to specify the context in which the node-style function runs, you can pass the context as an argument to promisify or bind the context to the function before wrapping it with promisify, as shown in Example 4-9.

Example 4-9. Specifying the execution context for a wrapped function

var person = {

name: 'Marie',

introNodeStyle: function (callback) {

var err = null;

callback(err, 'My name is ' + this.name);

}

};

var wrapper = Bluebird.promisify(person.introNodeStyle);

wrapper().then(function (greeting) {

console.log('promisify without second argument: ' + greeting);

});

var wrapperWithPersonArg = Bluebird.promisify(person.introNodeStyle, person);

wrapperWithPersonArg().then(function (greeting) {

console.log('promisify with a context argument: ' + greeting);

});

var wrapperWithBind = Bluebird.promisify(person.introNodeStyle.bind(person));

wrapperWithBind().then(function (greeting) {

console.log('promisify using function.bind: ' + greeting);

});

// Console output:

// promisify without second argument: Hello my name is

// promisify with a context argument: Hello my name is Marie

// promisify using function.bind: Hello my name is Marie

Only the wrappers using a bound function or where the context was provided as a second argument include a name. All the wrappers call person.introNodeStyle(), which builds a string containing this.name. However, the first wrapper created with an undefined second argument was run in the root object scope (the window object in a web browser), which does not have a name property. The next wrapper specifies the context by passing it as the second argument to promisify. And the last one used the function’s bind method to set the context to an object literal.

Caution

Be careful when wrapping functions that are intended to run as methods (i.e., in the context of a certain object.) Use the function’s bind or an equivalent wrapper to ensure the method is run in the expected context. Running methods in the wrong context may produce runtime errors or unexpected behavior.

Working with Collections of Promises

Bluebird provides promise-enabled versions of the map, reduce, and filter methods similar to the ones available for standard JavaScript arrays. Example 4-10 shows the filter and reduce methods at work.

Example 4-10. Using a promise-enabled filter and reduce

function sumOddNumbers(numbers) {

return numbers.filter(function removeEvenNumbers(num) {

return num % 2 == 1;

}).reduce(function sum(runningTotal, num) {

return runningTotal + num;

}, 0);

}

// Use sumOddNumbers as a synchronous function

var firstSum = sumOddNumbers([1, 2, 3, 4]);

console.log('first sum: ' + firstSum);

// Use sumOddNumbers as an async function

var promise = Bluebird.resolve([5, 6, 7, 8]);

sumOddNumbers(promise).then(function (secondSum) {

console.log('second sum: ' + secondSum);

});

// Console output:

// first sum: 4

// second sum: 12

The sumOddNumbers function accepts an array of numbers and uses filter to remove any even numbers. Then reduce is used to add together the remaining values. The function works regardless of whether it is passed a standard array or a promise that an array fulfilled. These promise-enabled methods allow you to write async code that looks identical to the synchronous equivalent.

Although the synchronous and async code looks the same and produces the same result, the execution sequence may differ. The promise-enabled map, reduce, and filter methods invoke their callbacks for each value as soon as possible. When the array contains a promise, the callback is not invoked for that element until the promise is resolved. For map and filter that means the callbacks can receive values in a different order than they appear in the array. Example 4-11 shows a map passing values to the callback out of order.

Example 4-11. Eager invocation of aggregate functions

function resolveLater(value) {

return new Bluebird(function (resolve, reject) {

setTimeout(function () {

resolve(value);

}, 1000);

});

};

var numbers = Bluebird.resolve([

1,

resolveLater(2),

3

]);

console.log('Square the following numbers...');

numbers.map(function square(num) {

console.log(num);

return num * num;

}).then(function (result) {

console.log('The squares of those numbers are...');

console.log(result.join(', '));

});

// Console output:

// Square the following numbers...

// 1

// 3

// 2

// The squares of those numbers are...

// 1, 4, 9

When map is invoked it receives an array whose second element is an unresolved promise. The other two elements are numbers that are immediately passed to the the square callback. After fulfilling the second promise, its value is passed to square. Once square processes all the values, an array that is identical to the one that the synchronous array.map() function would return resolves the promise returned by map.

Since using array.map() or Bluebird.map() in this example produces the same result, it doesn’t matter what order the values are passed to the callbacks. That only works as long as the callback used for map does not have any side effects. The map function is meant to convert one value to another using a callback. Adding side effects to the map callback conflicts with the intended use. The same thing applies to the reduce and filter functions. Avoid trouble by keeping any callbacks these functions use free from side effects.

Manipulating Fulfillment Values

When chaining together promises to execute a series of steps, the fulfillment value of one step often provides a value needed in the next step. This progression generally works well, but sometimes multiple subsequent steps require the same value. In that case you need a way to expose the fulfillment value to additional steps.

Imagine a series of database commands that all require a connection object. If the connection is obtained through a promise in the chain it will not be available to other steps in the chain by default. You can expose the connection to other steps by assigning it to a variable in the enclosing scope, as shown in Example 4-12.

Example 4-12. Exposing a fulfillment value using the enclosing scope

var connection; // Declare in outer scope for use in multiple functions

getConnection().then(function (con) {

connection = con;

return connection.insert('student', {name: 'Bobby'});

}).then(function () {

return connection.count('students');

}).then(function (count) {

console.log('Number of students: ' + count);

return connection.close();

});

The promise chain in the example consists of three callbacks. The first callback inserts a student, the second callback fetches the number of students, and the third reports the number in the console. In order for all three callbacks to use the connection object, the fulfillment value fromgetConnection is assigned to the connection variable in the enclosing scope.

There are ways to expose the connection object to the other callbacks without creating a variable in the outer scope. Bluebird promises have a return method that returns a new promise that is resolved by the argument it is given. Example 4-13 is a revised snippet using return to pass the connection to the second callback.

Example 4-13. Passing on a value using promise.return()

getConnection().then(function (connection) {

return connection

.insert('student', {name: 'Bobby'})

.return(connection);

}).then(function (connection) { ...

For this scenario you could also use the tap method of a Bluebird promise to get the connection object to the second callback. The tap method allows you to insert a callback into the promise chain while passing the fulfillment value it receives on to the next callback.

Example 4-14. Passing on a value using promise.tap()

getConnection().tap(function (connection) {

return connection.insert('student', {name: 'Bobby'});

}).then(function (connection) { //...

Think of tap as tapping into a line without interfering with the existing flow. Use tap to add supplementary functions into a promise chain. A practical use for tap would be adding a logging statement into a promise chain, as shown in Example 4-15.

Example 4-15. Supplementing a chain with promise.tap()

function countStudents() {

return getConnection().then(function (connection) {

return connection.count('students');

}).tap(function (count) {

console.log('Number of students: ' + count);

});

}

countStudents().then(function (count) {

if (count > 24) console.log('Classroom has too many students');

});

// Console output:

// Number of students: 25

// Classroom has too many students

The call to tap in the countStudents function can be added or removed without affecting the outcome of the function.

Using return or tap masks the fulfillment value that the callback would otherwise return. That worked well in the previous examples because the results of connection.insert() or console.log() were not needed. In situations where they are needed, you can supplement the original fulfillment value with additional items in a callback by passing them in an array to Promise.all(), as shown in Example 4-16. Then the items in the array can be split into separate arguments of a callback using spread.

Example 4-16. Passing in multiple values with Promise.all()

getConnection().then(function (connection) {

var promiseForCount = connection.count('students');

return Promise.all([connection, promiseForCount]);

}).spread(function (connection, count) {

console.log('Number of students: ' + count);

return connection.close();

});

Promises in jQuery

In jQuery, deferred objects represent async operations. A deferred object is like a promise whose resolve and reject functions are exposed as methods. Example 4-17 shows a loadImage function using a deferred object.

Example 4-17. Simple deferred object in jQuery

function loadImage(url) {

var deferred = jQuery.Deferred();

var img = new Image();

img.src = url;

img.onload = function () {

deferred.resolve(img);

};

img.onerror = function (e) {

deferred.reject(e);

};

return deferred;

}

The standard Promise API encapsulates the resolve and reject functions inside the promise. For example, if you have a promise object p, you cannot call p.resolve() or p.reject() because those functions are not attached to p. Any code that receives a reference to p can attach callbacks using p.then() or p.catch() but the code cannot control whether p gets fulfilled or rejected.

By encapsulating the resolve and reject functions inside the promise you can confidently expose the promise to other pieces of code while remaining certain the code cannot affect the fate of the promise. Without this guarantee you would have to consider all code that a promise was exposed to anytime a promise was resolved or rejected in an unexpected way.

Using deferreds does not mean you have to expose the resolve and reject methods everywhere. The deferred object also exposes a promise that can be given to any code that should not be calling resolve or reject.

Compare the two functions in Example 4-18. The first is a revised version of loadImage that returns deferred.promise() and the second is the equivalent function implemented with a standard promise.

Example 4-18. Deferred throws synchronous errors

function loadImage(url) {

var deferred = jQuery.Deferred();

// ...

return deferred.promise();

}

function loadImageWithoutDeferred(url) {

return new Promise(function resolver(resolve, reject) {

var image = new Image();

image.src = url;

image.onload = function () {

resolve(image);

};

image.onerror = reject;

});

}

The main difference between the two functions is that the function used as a deferred could throw a synchronous error while any errors thrown inside the function with the Promise constructor are caught and used to reject the promise. Promises created from jQuery deferreds do not conform to the standard ES6 Promise API or behavior. Some method names on jQuery promises differ from the spec; for example, [jQueryPromise].fail() is the counterpart to [standardPromise].catch().

A more important difference is in handling errors in the onFulfilled and onRejected callbacks. Standard promises automatically catch any errors thrown in these callbacks and convert them into rejections. In jQuery promises, these errors bubble up the call stack as uncaught exceptions.

Also, jQuery will invoke an onFulfilled or onRejected callback synchronously if settling a promise before the callback is registered. This creates the problems with multiple execution paths described in Chapter 1.

For more differences between standard promises and the ones jQuery provides, refer to a document written by Kris Kowal titled Coming from jQuery.

Some developers may prefer the style of deferred objects or find them easier to understand. However, a more significant case for using a deferred is in a situation where you cannot resolve the promise in the place it is created.

Suppose you are using a web worker to perform long-running tasks. You can use promises to represent the outcome of the tasks. The code that receives the response from the web worker will resolve the promise so it needs access to the appropriate resolve and reject functions.Example 4-19 demonstrates this.

Example 4-19. Managing web worker results with deferred objects

// Contents of task.js

onmessage = function(event) {

postMessage({

status: 'completed',

id: event.data.id,

result: 'some calculated result'

});

};

// Contents of main.js

var worker = new Worker('task.js');

var deferreds = {};

var counter = 0;

worker.onmessage = function (msg) {

var d = deferreds[msg.data.id];

d.resolve(msg.data.result);

};

function background(task) {

var id = counter++;

var deferred = jQuery.Deferred();

deferreds[id] = deferred; // Store deferred for later resolution

console.log('Sending task to worker: ' + task);

worker.postMessage({

id: id,

task: task

});

return deferred.promise(); // Only expose promise to calling code

}

background('Solve for x').then(function (result) {

console.log('The outcome is... ' + result);

}).fail(function(err) {

console.log('Unable to complete task');

console.log(err);

});

// Console output:

// Sending task to worker: Solve for x

// The outcome is... some calculated result

Example 4-19 shows the contents of two files: tasks.js for the web worker and main.js for the script that launches the worker and receives the results. The worker script is extremely simple for this example. Any time it receives a message it replies with an object containing the id of the original request and a hard-coded result. The background function in the main script returns a resolved promise once the worker sends a “completed” message for that task. Since processing the completed message occurs outside the background function that creates the promise, a deferred object is used to expose a resolve function to the onCompleted callback.

NOTE

For detailed information on web workers, refer to Web Workers: Multithreaded Programs in JavaScript by Ido Green (O’Reilly.)

One final note for this section: if you wish to use a deferred object without jQuery, it is easy to create one using the standard Promise API, as shown in Example 4-20.

Example 4-20. Creating a deferred object using a standard Promise constructor

function Deferred() {

var me = this;

me.promise = new Promise(function (resolve, reject) {

me.resolve = resolve;

me.reject = reject;

});

}

var d = new Deferred();

Summary

Libraries offer an extended set of features for working with promises. Many of these are convenience functions that save you from mixing the plumbing with your code. This chapter covered a number of features that the Bluebird library provides, although Q and other libraries offer similar functionality and are also popular among developers. It also explained the deferred objects jQuery uses. These objects expose promises that have some significant behavioral differences compared to the ES6 standard.