Combining ECMAScript 6 Features with Promises - JavaScript with Promises (2015)

JavaScript with Promises (2015)

Chapter 6. Combining ECMAScript 6 Features with Promises

ECMAScript 6 has a number of language features that complement promises. This chapter shows how destructuring, arrow functions, iterators, and generators simplify your promise-related code. However, this is not a full explanation of these features or ES6. It is merely a starting point for taking advantage of ES6 in your code.

The new syntax that these features require causes errors in JavaScript environments that do not support them. Unlike the Promise API that is unobtrusively polyfilled, code that uses the new syntax must be modified in order to run in older environments. You can automate the modification by transpiling the code into something equivalent that runs in older environments. However, multiple JavaScript environments such as Google Chrome and Mozilla Firefox already support some of these features, such as generators. The ECMAScript 6 compatibility table maintained by Juriy Zaytsev (a.k.a. kangax) on GitHub is a good place to see which ES6 features are available on your target platform.

Destructuring

Destructuring provides a syntax for extracting values from arrays or objects into individual variables. Instead of writing individual assignment statements for each variable, destructuring allows you to assign the values for multiple variables in a single statement. Examples 6-1 and 6-2 present destructuring using an array and an object.

Example 6-1. Array destructuring

var numbers = [10, 20];

var [n1, n2] = numbers; // destructuring

console.log(n1); // 10

console.log(n2); // 20

Example 6-2. Object destructuring

var position = {x: 50, y: 100};

var {x, y} = position; // destructuring

console.log(x); // 50

console.log(y); // 100

The destructuring syntax can also be used when declaring function parameters. In Chapter 3, an example from the WHATWG Streams specification used a promise fulfilled with an object containing two properties: value and done. Example 6-3 is a comparison of how the onFulfilledcallback can be written with destructuring.

Example 6-3. Object destructuring with function parameters

// Without destructuring

reader.read().then(function (result) {

// ... Use result.value and result.done

});

// Using destructuring

reader.read().then(function ({value, done}) {

// ... Use done and value directly

});

Array destructuring also works in function parameters. Example 4-16 mapped values from an array to parameters called enabled and lastLogin using bluebirdPromise.spread(). Example 6-4 shows the equivalent code using destructuring.

Example 6-4. Array destructuring with function parameters

// Without destructuring

getAccountStatus().then(function (status) {

var enabled = status[0];

var lastLogin = status[1];

// ...

});

// Using destructuring

getAccountStatus().then(function ([enabled, lastLogin]) {

// ... Use enabled and lastLogin directly

});

Array destructuring is also useful for handling the fulfillment value of Promise.all(), as seen in Example 6-5.

Example 6-5. Destructuring the fulfillment value from Promise.all()

Promise.all([promise1, promise2]).then(function ([result1, result2]) {

// ...

});

Arrow Functions

The arrow function syntax is like shorthand for declaring anonymous functions. In lieu of a full explanation of this new syntax, let’s create a simple example using an arrow function and then apply it to promises.

Arrow functions are useful for declaring callbacks that you would typically write as inline functions. In “Parallel Execution”, an array of bank and credit card accounts was mapped to requests for their current balance using code similar to Example 6-6.

Example 6-6. Using array.map() with an inline callback

var requests = accounts.map(function (account) {

return ajax('/balances/' + account);

});

The code in Example 6-6 can be rewritten as it appears in Example 6-7 using an arrow function.

Example 6-7. Using array.map() with an arrow function

var requests = accounts.map(account => ajax('/balances/' + account));

The new syntax always omits the function keyword. When there is only one parameter, the parentheses around the parameter may also be dropped. And when the body of the function consists of a single return statement, the enclosing braces and the word return can be left out as well. The noise created by the traditional function syntax is stripped away, leaving a concise piece of code.

Now let’s use the arrow function syntax in a chain of promises. The section “Functional Composition” used the code in Example 6-8 to create an image processing pipeline.

Example 6-8. Concise pipeline (repeated from earlier chapter)

function processImage(image) {

// Image is always last parameter preceded by any configuration parameters

var customScaleToFit = scaleToFit.bind(null, 300, 450);

var customWatermark = watermark.bind(null, 'The Real Estate Company');

return Promise.resolve(image)

.then(customScaleToFit)

.then(customWatermark)

.then(grayscale);

}

Example 6-9 shows a version of the pipeline using arrow functions.

Example 6-9. Concise pipeline with arrow functions

function processImage(image) {

return Promise.resolve(image)

.then(image => scaleToFit(300, 450, image))

.then(image => watermark('The Real Estate Company', image))

.then(image => grayscale(image))

.then(({src}) => console.log('Processing completed for ' + src));

}

This version also includes a logging function at the end of the chain that uses destructuring to directly access the src property of the image.

Using arrow functions allows you to create one-line callbacks for each of the steps in the pipeline without creating prebound functions at the top of processImage. This is just another way to accomplish the same thing as the previous version of processImage; the implementation style is a matter of preference.

Iterables and Iterators

ES6 introduces the ability to iterate through multiple items that an object provides. This is similar to walking through the items in an array using an index or through the properties of an object using for…in. However, iterators differ from both of these because they allow any object to provide an arbitrary series of items as opposed to one that is based on the object’s keys. For instance, an object called linkedlist could provide all the items in the list and an object called tree could expose all of its nodes.

One can access the items through a combination of two interfaces (also known as protocols), which are predefined sets of functions with specific names and behaviors. Objects that expose a series of items are known as iterables. These objects provide an iterator that exposes one item at a time and indicates when the series is exhausted. Thus the two interfaces are named iterable and iterator.

Objects that want to expose a series of items can implement the iterable interface by defining a function whose name is the value of Symbol.iterator, that is, object[Symbol.iterator] = function () {…}. This function should return an object with the iterator interface.

The iterator interface has one method named next. The method returns an object with two properties named value and done. The value represents the current item in the iteration and the done property is a flag to indicate when there are no more values available from the iterator.

Arrays are iterables so they contain the Symbol.iterator method, as shown in Example 6-10.

Example 6-10. Using the iterable interface of an array

var array = [1, 2];

var iterator = array[Symbol.iterator]();

iterator.next(); // {value: 1, done: false}

iterator.next(); // {value: 2, done: false}

iterator.next(); // {value: undefined, done: true}

How do iterables relate to promises? The Promise.all() and Promise.race() functions both accept iterables. Although an array is probably the most common type of iterable you would use with these functions, other options are available. For instance, the Set datatype in ES6 is also an iterable. A set is a collection of items that does not contain duplicates. You can pass a set to Promise.all() or you can use a custom iterable by implementing the interface on an object you define.

In addition to working with Promise.all() and Promise.race(), iterators work closely with ES6 generators, as described in the next section.

Generators

ES6 includes a feature called generators that allows you to write async code that looks synchronous. Generators are not easy to explain in a few sentences. Let’s begin with the end in mind by showing the style of async code that can be written when you combine promises and generators. Then we’ll work through the individual concepts required to understand that code.

Synchronous Style

Let’s use the async loadImage function in Example 6-11 as a starting point for the discussion.

Example 6-11. Managing asynchronous image loading using a promise

loadImage('thesis_defense.png').then(function (img) {

document.body.appendChild(img);

}).catch(function (e) {

console.log('Error occurred while loading image');

console.log(e);

});

Callbacks passed to then and catch handle the outcome of loadImage because loadImage returns a promise. If the image was loaded synchronously the calling code could be written as shown in Example 6-12.

Example 6-12. Hypothetical use of loadImage as a synchronous function

try {

var img = loadImage('thesis_defense.png');

document.body.appendChild(img);

} catch (err) {

console.log('Error occured while loading the image');

console.log(err);

}

The synchronous version of loadImage returns the image for immediate use and a traditional try/catch block helps perform error handling. When you combine generators and promises you can write code that looks like this even though the functions being called are asynchronous.Example 6-13 uses the asynchronous version of loadImage with a generator.

Example 6-13. Using a promise with code that looks synchronous

async(function* () {

try {

var img = yield loadImage('thesis_defense.png');

document.body.appendChild(img);

} catch (err) {

console.log('Error occurred while loading the image');

console.log(err);

}

})();

Ignoring the first line for a moment, we see that the remaining code is identical to the synchronous equivalent except for one yield keyword added before the call to loadImage. This simple change allows you to write async code in this fashion. The rest of the chapter explains how that is possible.

Generators and Iterators

A generator is a special type of function that can pause its execution to pass values back to its caller and later resume executing where it left off. This ability is useful for generating a series of values. The Fibonacci sequence can be used as an example. Without using generators it can be computed as shown in Example 6-14.

Example 6-14. Computing a series of values without using a generator

var a = 0;

var b = 1;

function fib() {

b = a + b;

a = b - a;

return b;

}

var i;

for (i = 0; i < 5; i++) console.log(fib());

// Console output:

// 1

// 2

// 3

// 5

// 8

The fib function tracks the last two values used in the sequence and adds them together every time it is called to calculate the next value. Example 6-15 shows the equivalent code using a generator.

Example 6-15. Computing a series of values using a generator

function* fib() {

var a = 0;

var b = 1;

while (true) {

yield a + b;

b = a + b;

a = b - a;

}

}

var i;

var result;

var iterator = fib();

for (i = 0; i < 5; i++) {

result = iterator.next();

console.log(result.value);

}

// Console output is identical to the previous example

The fib function is now a generator, which is indicated by adding the * at the end of the function keyword. When the generator is called, the JavaScript engine does not start running the code inside fib as it would with a normal function. Instead the call to fib returns an iterator. The iterator is used to pause and resume execution of the generator and pass values between the generator and the calling code.

The code inside fib starts running the first time iterator.next() is called. Execution continues until the yield keyword. At that point the function pauses and sends the result of the yield expression back to the calling code as the return value of iterator.next(). The result is an object that provides the outcome of the yield statement in a property named value.

When iterator.next() is called again the code inside fib resumes execution on the line after the yield statement. The values of a and b are updated and the next iteration of the while loop hits the yield statement, which repeats the pause and send behavior for another number in the sequence.

A generator may contain multiple yield statements but in this case it has one yield placed inside an infinite while loop. The loop allows the iterator to provide an indefinite amount of Fibonacci numbers. In the previous example the calling code stopped making requests after five values.

Example 6-15 introduced three concepts: the generator declaration syntax, the iterator, and the yield keyword. That’s a lot to comprehend at once but all three are necessary to create a basic example. Consider reviewing the previous snippet and explanation until you are comfortable with these concepts.

Sending Values to a Generator

Not only can values be passed from the generator back to the calling code, they can also be passed from the calling code into the generator. The iterator.next() method accepts a parameter that is used as a result of the yield expression inside the generator. Example 6-16 demonstrates passing a value into the generator. In this case a function counts things one at a time by default but can be adjusted to count in any increment.

Example 6-16. Passing values into the generator

function* counter() {

var count = 0;

var increment = 1;

while (true) {

count = count + increment;

increment = (yield count) || increment;

}

}

var iterator = counter();

console.log(iterator.next().value); // 1

console.log(iterator.next().value); // 2

console.log(iterator.next().value); // 3

console.log(iterator.next(10).value); // 13 <- Start counting by 10

console.log(iterator.next().value); // 23

console.log(iterator.next().value); // 33

The fourth call to iterator.next() sets the increment value to 10. All the other calls to iterator.next() pass a value of undefined by not providing an explicit argument.

A generator can also declare parameters similar to a traditional function. The values for these parameters are set when the iterator is created and they may act as a configuration for the iterator. Example 6-17 is a revised version of the counter whose initial increment can be set by a parameter.

Example 6-17. Configuring an iterator with an initial parameter

function* counter(increment) {

var count = 0;

increment = increment || 1;

while (true) {

count = count + increment;

increment = (yield count) || increment;

}

}

var evens = counter(2);

console.log('Even numbers'); // Even numbers

console.log(evens.next().value); // 2

console.log(evens.next().value); // 4

console.log(evens.next().value); // 6

var fives = counter(5);

console.log('Count by fives'); // Count by fives

console.log(fives.next().value); // 5

console.log(fives.next().value); // 10

console.log(fives.next().value); // 15

Two iterators are created from counter with different configurations. Creating iterators from a generator is similar to creating objects from a constructor function. Each iterator maintains its own state to apply general code, such as counting in predefined increments, to the specific cases of counting in even numbers or counting by fives.

We’ve discussed how values can be passed to generators using the initial parameters and as an argument to iterator.next(). However, there are two cases where the argument to iterator.next() is ignored. The argument is always ignored the first time iterator.next() is called.Example 6-18 shows the value being ignored followed by an explanation of why it happens.

Example 6-18. The parameter in the first call to iterator.next() is always ignored

// Same function* counter as previous example

function* counter(increment) {

var count = 0;

increment = increment || 1;

while (true) {

count = count + increment;

increment = (yield count) || increment;

}

}

var iterator = counter(5); // <- Initial increment is 5

console.log(iterator.next(3).value); // 5 <- 3 is ignored

console.log(iterator.next().value); // 10

console.log(iterator.next(200).value); // 210 <- Increment by 200

console.log(iterator.next().value); // 410

The number 3 passed in the first call to iterator.next() has no effect in the code because the generator syntax and API do not provide a way to receive this value. All values that the next method passes to the generator are received when the code resumes after a yield statement as val = yield. However, the first call to next does not resume the function from a paused state. The first call starts the initial execution of the function and there is no mechanism for receiving a value at that point. In a traditional function the parameters serve that purpose but in a generator the call that creates the iterator sets the parameter values.

The other case where an argument to iterator.next() is ignored is after the function returns. All the previous examples contain infinite loops that paused the function to send back a value. When a generator function returns from execution in the traditional sense as opposed to pausing on yield, there is no way to receive more data from the iterator. Example 6-19 is a generator that returns after filtering objects in an array.

Example 6-19. Finite iterations

function* match(objects, propname, value) {

var i;

var obj;

for (i = 0; i < objects.length; i++) {

obj = objects[i];

if (obj[propname] === value) yield obj;

};

}

var animals = [

{ type: 'bird', legs: 2 },

{ type: 'cat', legs: 4 },

{ type: 'dog', legs: 4 },

{ type: 'spider', legs: 8 }

];

var iterator = match(animals, 'legs', 4);

console.log(iterator.next().value.type); // value is an animal

console.log(iterator.next().value.type); // value is an animal

console.log(iterator.next().value); // value is undefined

// Console output:

// cat

// dog

// undefined

The match generator accepts an array of objects along with a property name and value used to filter the objects. Any object with a matching property and value is yielded back to the calling code. After checking all the objects, the function returns. Any value returned by the function is used in the final result. And any result objects that next returns after that point have their value property set as undefined.

The result that next returns also exposes a done property to indicate when the iterator has finished executing. The property is useful for looping through the results as shown in Example 6-20.

Example 6-20. Looping through iterations

// Substitute for iterator and console.log in previous example

iterator = match(animals, 'legs', 4);

while ((result = iterator.next()).done !== true) {

console.log(result.value.type);

}

// Console output:

// cat

// dog

Each turn of the while loop assigns the next iteration result to an object and checks the done flag. This is a vast improvement over hardcoding for the expected number of results, but there is a more elegant way to write this loop. A new for…of construct, as shown in Example 6-21, allows you to implicitly manage the iterator. Use for…of if you are dealing with a finite number of iterations and do not need to pass values back to the generator.

Example 6-21. Using an implicit iterator created by for…of

// Better substitute for iterator and loop

for (animal of match(animals, 'legs', 4)) {

console.log(animal.type);

}

Sending Errors to a Generator

An iterator can cause an error to be thrown when execution resumes inside a generator. Example 6-22 is a contrived scenario to demonstrate the functionality. The example prints hello in a series of languages that a generator provides. An error is thrown when there is no translation available for the language. Note the call to iterator.throw() at the bottom of the example.

Example 6-22. Throwing errors with the iterator

function* languages() {

try {

yield 'English';

yield 'French';

yield 'German';

yield 'Spanish';

} catch (error) {

console.log(error.message);

}

}

var greetings = {

English: 'Hello',

French: 'Bonjour',

Spanish: 'Hola'

};

var iterator = languages();

var result;

var word;

while ((result = iterator.next()).done !== true) {

word = greetings[result.value];

if (word) console.log(word);

else iterator.throw(Error('Missing translation for ' + result.value));

}

// Console output:

// Hello

// Bonjour

// Missing translation for German

When the iterator yields “German” there is no translation found for that language so an error is sent to the generator using iterator.throw(). The error is thrown inside the generator where the yield 'German' expression is evaluated. The yield 'Spanish' statement is skipped, as the error immediately falls to the catch block. Although sending an error back to the generator in this example is not useful, this ability is needed to write synchronous-looking code using promises and generators.

Practical Application

Now let’s revisit Example 6-13 from “Synchronous Style” to see how it works. Example 6-23 repeats the code here for convenience.

Example 6-23. Using a promise with code that looks synchronous (repeated from earlier)

async(function* () {

try {

var img = yield loadImage('thesis_defense.png');

document.body.appendChild(img);

} catch (err) {

console.log('caught in async routine');

console.log(err);

}

})();

The loadImage function is called in the body of a generator. Although loadImage returns a promise, the yield statement inside the generator returns the fulfillment value of that promise: an image object in this case. How is that possible? Instead of directly creating an iterator from the generator and invoking iterator.next(), the generator is passed to async, which returns a wrapper function. When the wrapper is invoked it intercepts any promise that the generator yields and waits for it to settle. Once the promise is fulfilled its value is passed into the generator. If the promise is rejected its rejection reason is thrown inside the generator.

There are several ways to implement the async function. Example 6-24 shows one way based on code written by Forbes Lindesay on promisejs.org.

Example 6-24. Sample async wrapper

function async(generator) {

return function () {

var iterator = generator.apply(this, arguments);

function handle(result) {

if (result.done) return Promise.resolve(result.value);

return Promise.resolve(result.value).then(function (res) {

return handle(iterator.next(res));

}, function (err) {

return handle(iterator.throw(err));

});

}

try {

return handle(iterator.next());

} catch (ex) {

return Promise.reject(ex);

}

};

}

The wrapper function also returns a promise that the return value of the generator fulfills or any unhandled error rejects. This behavior is identical to any callback registered with promise.then() or promise.catch(). If you prefer handling errors with promise.catch instead of traditional try/catch blocks, you can attach a catch to the promise that the async wrapper returns, as shown in Example 6-25.

Example 6-25. Replacing try/catch with promise.catch()

async(function* () {

var img = yield loadImage('thesis_defense.png');

document.body.appendChild(img);

})().catch(function (err) {

console.log('caught in rejection handler');

console.log(err);

});

Some promise libraries provide a function similar to async, such as Q.async() and Bluebird.coroutine(). Using these functions to wrap a single call to a promise is probably overkill, but this style is useful when dealing with multiple asynchronous steps in a single function because you can replace all the promise.then() callbacks with synchronous return values.

There is a plan to introduce async and await keywords in ECMAScript 7, as shown in Example 6-26, that will remove the need for the async(generator) pattern described in this section.

Example 6-26. Using async and await as proposed in ES7

async function () {

try {

var img = await loadImage('thesis_defense.png');

document.body.appendChild(img);

} catch (err) {

console.log('caught in rejection handler');

console.log(err);

}

});

The proposed syntax allows you to declare an async function instead of using a generator. Since the function is not a generator, the yield keyword is replaced with await. All other parts of the code are identical and this function behaves the same as its ES6 counterpart. The purpose is to remove the burden of supplying a boilerplate async function.

Summary

This chapter showed how some of the new language features in ES6 can be used with promises. These features all allow you to write less code to accomplish the same outcome. We began with simplifying access to fulfillment values using destructuring, followed by concise callback declarations using arrow functions. And we concluded with how iterators and generators can be used to treat async functions that return promises as synchronous code.

This chapter also concludes the book. We started with the fundamentals of asynchronous programming in JavaScript and worked through the core concepts in Promises and how to utilize them in a wide variety of scenarios. At this point you should be prepared to confidently manage async tasks with Promises, absorb new promise-based APIs such as Service Workers or Streams, and even create your own promise-based API.