);
});
return Promise.all(alwaysFulfilled);
}
// Update status message once all requests finish settled(requests).then(function (outcomes) { var count = 0;
outcomes.forEach(function (outcome) {
if (outcome.state == 'fulfilled') count++;
});
console.log(count + ' out of ' + outcomes.length + ' balances were updated');
});
// Console output (varies based on requests):
// 2 out of 3 balances were updated
The settled function consolidates an array of promises into a single promise that is fulfilled once all the promises in the array are settled. An array of objects that indicate the outcome of each promise fulfills the new promise. In this example, the array of outcomes is reduced1 to a single value representing the number of requests that suc‐
ceeded.2
Sequential Execution Using Loops or Recursion
You can dynamically build a chain of promises to run tasks in sequential order (i.e., each task must wait for the preceding task to finish before it begins.) Most of the examples so far have demonstrated sequential chains of then calls built with a pre-defined number of steps. But it is common to have an array where each item requires its own async task, like the code in Example 3-6 that looped through an array of accounts to get the balance for each one. Those balances were retrieved in parallel but there are times when you want to run tasks serially. For instance, if each task requires significant bandwidth or computation, you may want to throttle the amount of work being done.
Before building a sequential chain, let’s start with code that runs a set of tasks in par‐
allel, as shown in Example 3-9, based on the items in an array similar to Example 3-6.
Example 3-9. Running tasks in parallel using a loop
var products = ['sku-1', 'sku-2', 'sku-3'];
products.forEach(function (sku) { getInfo(sku).then(function (info) { console.log(info)
});
});
function getInfo(sku) {
console.log('Requested info for ' + sku);
return ajax(/*someurl for sku*/);
}
// Console output:
// Requested info for sku-1 // Requested info for sku-2 // Requested info for sku-3 // Info for sku-1
// Info for sku-2 // Info for sku-3
This code iterates through an array of products and calls the getInfo function for each one. The beginning of each request is logged to the console inside getInfo and the outcome of each request is logged inside the loop after completing the request.
You can see the requests are run in parallel because the code inside the forEach does not use any promises that previous iterations of the loop created. The order of the console output also demonstrates the parallel nature of the code. All three requests are made before the first result is received.
Let’s move from parallel tasks to sequential chains. The code we’ll use to do that can be daunting if you are unfamiliar with the array reduce function, which distills the elements of an array to a single value. Example 3-10 provides a snippet to serve as an introduction/refresher on how reduce is used.
Example 3-10. Overview of array.reduce
finalResult = array.reduce(function (previousValue, currentValue) { // Create a result using the previousValue and currentValue
// return the result which will be used as the previousValue in the next loop return previousValue + currentValue;
}, initialValue) // Used with first element
The reduce function accepts a callback that is invoked for each element in the array.
It receives the previous value returned from the callback and the current element in the array. The previous value in the callback is seeded with an initial value the first
Sequential Execution Using Loops or Recursion | 31
time the callback is invoked. The return value for reduce is whatever the callback returns when it is invoked for the last element in the array.
Example 3-11 uses reduce to calculate the sum of all the numbers in an array.
Example 3-11. Simple array.reduce to sum numbers
var numbers = [2, 4, 6];
var sum = numbers.reduce(function (sum, number) { return sum + number;
}, 0);
console.log(sum);
// Console output:
// 12
You could write some code with a for loop that would accomplish the same thing as reduce but it would be clunky by comparison. Now let’s get back to running tasks sequentially using reduce.
Example 3-12 uses the same products array and getInfo function from earlier code to request and display information. However, no request is started until the previous one completes. Although this could be done by calling the reduce function on products directly, the logic has been abstracted into a function called sequence. Example 3-12. Build a sequential chain using a loop
// Build a sequential chain of promises from the elements in an array function sequence(array, callback) {
return array.reduce(function chain(promise, item) { return promise.then(function () {
return callback(item);
});
}, Promise.resolve());
};
var products = ['sku-1', 'sku-2', 'sku-3'];
sequence(products, function (sku) {
return getInfo(sku).then(function (info) { console.log(info)
});
}).catch(function (reason) { console.log(reason);
});
function getInfo(sku) {
console.log('Requested info for ' + sku);
return ajax(/*someurl for sku*/);
}
// Console output:
// Requested info for sku-1 // Info for sku-1
// Requested info for sku-2 // Info for sku-2
// Requested info for sku-3 // Info for sku-3
Skip the implementation of sequence for a moment and look at how it is used. An array of products is passed in along with a callback that is invoked once for each product in the array. If the callback returns a promise, the next callback is not invoked until that promise is fulfilled. The console output shows that the requests are run sequentially.
The sequence function encapsulates the details of chaining promises to dynamically sequence tasks. It iterates over the array by calling reduce and seeding the previous value with a resolved promise. The chain function given to reduce always returns a promise that the return value of the callback passed into sequence resolves. The cycle continues for each element until exhausting the array and returning the last promise in the chain. The calling code attaches a catch handler to that promise to conven‐
iently handle any problems.
You can also construct a sequence of tasks from a list using recursion by replacing the previous sequence implementation in Example 3-12 with the code in Example 3-13.
Example 3-13. Build sequential chain using recursion
// Replaces sequence in previous example with a recursive implementation function sequence(array, callback) {
function chain(array, index) {
if (index == array.length) return Promise.resolve();
return Promise.resolve(callback(array[index])).then(function () { return chain(array, index + 1);
});
}
return chain(array, 0);
};
// Console output is identical to the previous example
Here the reduce function from earlier is replaced by chain, which recursively calls itself for each element in the array. A risk in recursive programming is creating a stack overflow by making too many recursive calls in a row. Fortunately, that does not occur here because a separate turn of the event loop invokes the promise callbacks, so each recursive call to chain is at the top of the call stack.
Sequential Execution Using Loops or Recursion | 33
3The code in the spec uses object destructuring with an arrow function, which has been replaced by a tradi‐
tional function declaration here. Destructuring and arrow functions are discussed in Chapter 6.
Although using recursion in Example 3-12 has the same final outcome as building the chain with a loop, there is an interesting difference between the two approaches.
Using a loop builds the entire chain of promises at the outset without waiting for any of the promises to be resolved. The recursive approach adds to the chain on demand after resolving the preceding promise. A major benefit of the on-demand approach is the ability to decide whether to continue chaining promises based on the result from the preceding promise.
The last few examples have made a chain with a predefined number of steps based on the elements in an array. With recursion you can build a chain whose length is not determined in advance, as shown in Example 3-14. A great example for this case is included in the WHATWG Streams specification for performing I/O. The spec con‐
tains sample code for sequentially reading all the data from a stream in a series of chunks. Each call to read returns a promise fulfilled by an object with a value prop‐
erty containing a chunk of data and a done property indicating when the stream is exhausted.3
Example 3-14. Conditionally expanding a chain based on the outcome of a preceding promise
function readAllChunks(readableStream) { var reader = readableStream.getReader();
var chunks = [];
return pump();
function pump() {
return reader.read().then(function (result) { if (result.done) {
return chunks;
}
chunks.push(result.value);
return pump();
});
};
}
Here the pump function appends each chunk of data to an array and recursively calls itself until result.done signals there is no more data available.
You may not have an immediate need for building sequential promise chains, but it will inevitably occur. If you use a library to supplement standard promises, this func‐
tionality may be included. Libraries are discussed in more detail in Chapter 4.
Building long chains of promises may require significant amounts of memory. Be sure to instrument and test your code to guard against unexpected performance problems.