Promise.resolve()
: create a Promise fulfilled with a given valuePromise.reject()
: create a Promise rejected with a given value.then()
callbacks.catch()
and its callbackXMLHttpRequest
util.promisify()
Promise.all()
: concurrency and Arrays of Promises
In this chapter, we explore Promises, yet another pattern for delivering asynchronous results.
Recommended reading
This chapter builds on the previous chapter with background on asynchronous programming in JavaScript.
Promises are a pattern for delivering results asynchronously.
The following code is an example of using the Promise-based function addAsync()
(whose implementation is shown soon):
addAsync(3, 4)
.then(result => { // success
assert.equal(result, 7);
})
.catch(error => { // failure
assert.fail(error);
});
Promises are similar to the event pattern: There is an object (a Promise), where you register callbacks:
.then()
registers callbacks that handle results..catch()
registers callbacks that handle errors.A Promise-based function returns a Promise and sends it a result or an error (if and when it is done). The Promise passes it on to the relevant callbacks.
In contrast to the event pattern, Promises are optimized for one-off results:
.then()
and .catch()
, because they both return Promises. That helps with sequentially invoking multiple asynchronous functions. More on that later.What is a Promise? There are two ways of looking at it:
This is how you implement a Promise-based function that adds two numbers x
and y
:
function addAsync(x, y) {
return new Promise(
(resolve, reject) => { // (A)
if (x === undefined || y === undefined) {
reject(new Error('Must provide two parameters'));
} else {
resolve(x + y);
}
});
}
addAsync()
immediately invokes the Promise
constructor. The actual implementation of that function resides in the callback that is passed to that constructor (line A). That callback is provided with two functions:
resolve
is used for delivering a result (in case of success).reject
is used for delivering an error (in case of failure).Fig. 22 depicts the three states a Promise can be in. Promises specialize in one-off results and protect you against race conditions (registering too early or too late):
.then()
callback or a .catch()
callback too early, it is notified once a Promise is settled..then()
or .catch()
are called after the settlement, they receive the cached value.Additionally, once a Promise is settled, its state and settlement value can’t change, anymore. That helps make code predictable and enforces the one-off nature of Promises.
Some Promises are never settled
It is possible that a Promise is never settled. For example:
Promise.resolve()
: create a Promise fulfilled with a given valuePromise.resolve(x)
creates a Promise that is fulfilled with the value x
:
If the parameter is already a Promise, it is returned unchanged:
Therefore, given an arbitrary value x
, you can use Promise.resolve(x)
to ensure you have a Promise.
Note that the name is resolve
, not fulfill
, because .resolve()
returns a rejected Promise if its Parameter is a rejected Promise.
Promise.reject()
: create a Promise rejected with a given valuePromise.reject(err)
creates a Promise that is rejected with the value err
:
const myError = new Error('My error!');
Promise.reject(myError)
.catch(err => {
assert.equal(err, myError);
});
.then()
callbacks.then()
handles Promise fulfillments. It also returns a fresh Promise. How that Promise is settled depends on what happens inside the callback. Let’s look at three common cases.
First, the callback can return a non-Promise value (line A). Consequently, the Promise returned by .then()
is fulfilled with that value (as checked in line B):
Promise.resolve('abc')
.then(str => {
return str + str; // (A)
})
.then(str2 => {
assert.equal(str2, 'abcabc'); // (B)
});
Second, the callback can return a Promise p
(line A). Consequently, p
“becomes” what .then()
returns. In other words: the Promise that .then()
has already returned, is effectively replaced by p
.
Promise.resolve('abc')
.then(str => {
return Promise.resolve(123); // (A)
})
.then(num => {
assert.equal(num, 123);
});
Why is that useful? You can return the result of a Promise-based operation and process its fulfillment value via a “flat” (non-nested) .then()
. Compare:
// Flat
asyncFunc1()
.then(result1 => {
/*···*/
return asyncFunc2();
})
.then(result2 => {
/*···*/
});
// Nested
asyncFunc1()
.then(result1 => {
/*···*/
asyncFunc2()
.then(result2 => {
/*···*/
});
});
Third, the callback can throw an exception. Consequently, the Promise returned by .then()
is rejected with that exception. That is, a synchronous error is converted into an asynchronous error.
const myError = new Error('My error!');
Promise.resolve('abc')
.then(str => {
throw myError;
})
.catch(err => {
assert.equal(err, myError);
});
.catch()
and its callbackThe only difference between .then()
and .catch()
is that the latter is triggered by rejections, not fulfillments. However, both methods turn the actions of their callbacks into Promises in the same manner. For example, in the following code, the value returned by the .catch()
callback in line A becomes a fulfillment value:
const err = new Error();
Promise.reject(err)
.catch(e => {
assert.equal(e, err);
// Something went wrong, use a default value
return 'default value'; // (A)
})
.then(str => {
assert.equal(str, 'default value');
});
.then()
and .catch()
always return Promises. That enables us to create arbitrary long chains of method calls:
function myAsyncFunc() {
return asyncFunc1() // (A)
.then(result1 => {
// ···
return asyncFunc2(); // a Promise
})
.then(result2 => {
// ···
return result2 || '(Empty)'; // not a Promise
})
.then(result3 => {
// ···
return asyncFunc4(); // a Promise
});
}
Due to chaining, the return
in line A, returns the result of the last .then()
.
In a way, .then()
is the asynchronous version of the synchronous semicolon:
.then()
executes two asynchronous operations sequentially.You can also add .catch()
into the mix and let it handle multiple error sources at the same time:
asyncFunc1()
.then(result1 => {
// ···
return asyncFunction2();
})
.then(result2 => {
// ···
})
.catch(error => {
// Failure: handle errors of asyncFunc1(), asyncFunc2()
// and any (sync) exceptions thrown in previous callbacks
});
These are some of the advantages of Promises over plain callbacks when it comes to handling one-off results:
The type signatures of Promise-based functions and methods are cleaner: If a function is callback-based, some parameters are about input, while the one or two callbacks at the end are about output. With Promises, everything output-related is handled via the returned value.
Chaining asynchronous processing steps is more convenient.
Promises handle both asynchronous errors (via rejections) and synchronous errors: Inside the callbacks for new Promise()
, .then()
and .catch()
, exceptions are converted to rejections. In contrast, if you use callbacks for asynchronicity, exceptions are normally not handled for you, you have to do it yourself.
Promises are a single standard that is slowly replacing several, mutually incompatible alternatives. For example, in Node.js, many functions are now available in Promise-based versions. And new asynchronous browser APIs are usually Promise-based.
One of the biggest advantages of Promises involves not working with them directly: They are the foundation of async functions, a synchronous-looking syntax for performing asynchronous computations. Asynchronous functions are covered in the next chapter.
Seeing Promises in action helps with understanding them. Let’s look at examples.
Consider the following text file person.json
with JSON data in it:
Let’s look at two versions of code that reads this file and parses it into an object. First, a callback-based version. Second, a Promise-based version.
The following code reads the contents of this file and converts it to a JavaScript object. It is based on Node.js-style callbacks:
import * as fs from 'fs';
fs.readFile('person.json',
(error, text) => {
if (error) { // (A)
// Failure
assert.fail(error);
} else {
// Success
try { // (B)
const obj = JSON.parse(text); // (C)
assert.deepEqual(obj, {
first: 'Jane',
last: 'Doe',
});
} catch (e) {
// Invalid JSON
assert.fail(e);
}
}
});
fs
is a built-in Node.js module for file system operations. We use the callback-based function fs.readFile()
to read a file whose name is person.json
. If we succeed, the content is delivered via the parameter text
, as a string. In line C, we convert that string from the text-based data format JSON into a JavaScript object. JSON
is an object with methods for consuming and producing JSON. It is part of JavaScript’s standard library and documented later in this book.
Note that there are two error handling mechanisms: The if
in line A takes care of asynchronous errors reported by fs.readFile()
, while the try
in line B takes care of synchronous errors reported by JSON.parse()
.
The following code uses readFileAsync()
, a Promise-based version of fs.readFile()
(created via util.promisify()
, which is explained later):
readFileAsync('person.json')
.then(text => { // (A)
// Success
const obj = JSON.parse(text);
assert.deepEqual(obj, {
first: 'Jane',
last: 'Doe',
});
})
.catch(err => { // (B)
// Failure: file I/O error or JSON syntax error
assert.fail(err);
});
Function readFileAsync()
returns a Promise. In line A, we specify a success callback via method .then()
of that Promise. The remaining code in then
’s callback is synchronous.
.then()
returns a Promise, which enables the invocation of the Promise method .catch()
in line B. We use it to specify a failure callback.
Note that .catch()
lets us handle both the asynchronous errors of readFileAsync()
and the synchronous errors of JSON.parse()
, because exceptions inside a .then()
callback become rejections.
XMLHttpRequest
We have previously seen the event-based XMLHttpRequest
API for downloading data in web browsers. The following function promisifies that API:
function httpGet(url) {
return new Promise(
(resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onload = () => {
if (xhr.status === 200) {
resolve(xhr.responseText); // (A)
} else {
// Something went wrong (404 etc.)
reject(new Error(xhr.statusText)); // (B)
}
}
xhr.onerror = () => {
reject(new Error('Network error')); // (C)
};
xhr.open('GET', url);
xhr.send();
});
}
Note how the results and errors of XMLHttpRequest
are handled via resolve()
and reject()
:
This is how you use httpGet()
:
httpGet('http://example.com/textfile.txt')
.then(content => {
assert.equal(content, 'Content of textfile.txt\n');
})
.catch(error => {
assert.fail(error);
});
Exercise: Timing out a Promise
exercises/promises/promise_timeout_test.mjs
util.promisify()
util.promisify()
is a utility function that converts a callback-based function f
into a Promise-based one. That is, we are going from this type signature:
f(arg_1, ···, arg_n, (err: Error, result: T) => void) : void
To this type signature:
f(arg_1, ···, arg_n) : Promise<T>
The following code promisifies the callback-based fs.readFile()
(line A) and uses it:
import * as fs from 'fs';
import {promisify} from 'util';
const readFileAsync = promisify(fs.readFile); // (A)
readFileAsync('some-file.txt', {encoding: 'utf8'})
.then(text => {
assert.equal(text, 'The content of some-file.txt\n');
})
.catch(err => {
assert.fail(err);
});
Exercises:
util.promisify()
util.promisify()
: exercises/promises/read_file_async_exrc.mjs
util.promisify()
yourself: exercises/promises/my_promisify_test.mjs
All modern browsers support Fetch, a new Promise-based API for downloading data. Think of it as a Promise-based version of XMLHttpRequest
. The following is an excerpt of the API:
interface Body {
text() : Promise<string>;
···
}
interface Response extends Body {
···
}
declare function fetch(str) : Promise<Response>;
That means, you can use fetch()
as follows:
fetch('http://example.com/textfile.txt')
.then(response => response.text())
.then(text => {
assert.equal(text, 'Content of textfile.txt\n');
});
Exercise: Using the fetch API
exercises/promises/fetch_json_test.mjs
Rule for implementing functions and methods:
Don’t mix (asynchronous) rejections and (synchronous) exceptions
This makes your synchronous and asynchronous code more predictable and simpler, because you can always focus on a single error handling mechanism.
For Promise-based functions and methods, the rule means that they should never throw exceptions. Alas, it is easy to accidentally get this wrong. For example:
// Don’t do this
function asyncFunc() {
doSomethingSync(); // (A)
return doSomethingAsync()
.then(result => {
// ···
});
}
The problem is that, if an exception is thrown in line A, then asyncFunc()
will throw an exception. Callers of that function only expect rejections and are not prepared for an exception. There are three ways in which we can fix this issue.
We can wrap the whole body of the function in a try-catch
statement and return a rejected Promise if an exception is thrown:
// Solution 1
function asyncFunc() {
try {
doSomethingSync();
return doSomethingAsync()
.then(result => {
// ···
});
} catch (err) {
return Promise.reject(err);
}
}
Given that .then()
converts exceptions to rejections, we can execute doSomethingSync()
inside a .then()
callback. To do so, we start a Promise chain via Promise.resolve()
. We ignore the fulfillment value undefined
of that initial Promise.
// Solution 2
function asyncFunc() {
return Promise.resolve()
.then(() => {
doSomethingSync();
return doSomethingAsync();
})
.then(result => {
// ···
});
}
Lastly, new Promise()
also converts exceptions to rejections. Using this constructor is therefore similar to the previous solution:
// Solution 3
function asyncFunc() {
return new Promise((resolve, reject) => {
doSomethingSync();
resolve(doSomethingAsync());
})
.then(result => {
// ···
});
}
Most Promise-based functions are executed as follows:
The following code demonstrates that:
function asyncFunc() {
console.log('asyncFunc');
return new Promise(
(resolve, _reject) => {
console.log('new Promise()');
resolve();
});
}
console.log('START');
asyncFunc()
.then(() => {
console.log('.then()'); // (A)
});
console.log('END');
// Output:
// 'START'
// 'asyncFunc'
// 'new Promise()'
// 'END'
// '.then()'
We can see that the callback of new Promise()
is executed before the end of the code, while the result is delivered later (line A).
Benefits of this approach:
Starting synchronously helps avoid race conditions, because you can rely on the order in which Promise-based functions begin. There is an example in the next chapter, where text is written to a file and race conditions are avoided.
Chaining Promises won’t starve other tasks of processing time, because before a Promise is settled, there will always be a break, during which the event loop can run.
Promise-based functions consistently return results asynchronously. Not sometimes immediately, sometimes asynchronously. This kind of predictability makes code easier to work with.
More information on this approach
“Designing APIs for Asynchrony” by Isaac Z. Schlueter
Promise.all()
: concurrency and Arrays of PromisesConsider the following code:
const asyncFunc1 = () => Promise.resolve('one');
const asyncFunc2 = () => Promise.resolve('two');
asyncFunc1()
.then(result1 => {
assert.equal(result1, 'one');
return asyncFunc2();
})
.then(result2 => {
assert.equal(result2, 'two');
});
Using .then()
in this manner, executes Promise-based functions sequentially: Only after the result of asyncFunc1()
is settled, will asyncFunc2()
be executed.
The static method Promise.all()
helps execute Promise-based functions more concurrently:
Its type signature is:
The parameter promises
is an iterable of Promises. The result is a single Promise that is settled as follows:
In other words: You go from an iterable of Promises to a Promise for an Array.
Tip for determining how “concurrent” asynchronous code is: Focus on when asynchronous operations start, not on how their Promises are handled.
For example, each of the following functions executes asyncFunc1()
and asyncFunc2()
concurrently, because they are started at nearly the same time.
function concurrentAll() {
return Promise.all([asyncFunc1(), asyncFunc2()]);
}
function concurrentThen() {
const p1 = asyncFunc1();
const p2 = asyncFunc2();
return p1.then(r1 => p2.then(r2 => [r1, r2]));
}
On the other hand, both of the following functions execute asyncFunc1()
and asyncFunc2()
sequentially: asyncFunc2()
is only invoked after the Promise of asyncFunc1()
is fulfilled.
function sequentialThen() {
return asyncFunc1()
.then(r1 => asyncFunc2()
.then(r2 => [r1, r2]));
}
function sequentialAll() {
const p1 = asyncFunc1();
const p2 = p1.then(() => asyncFunc2());
return Promise.all([p1, p2]);
}
Promise.all()
is fork-joinPromise.all()
is loosely related to the concurrency pattern “fork join”. For example:
Promise.all([
// Fork async computations
httpGet('http://example.com/file1.txt'),
httpGet('http://example.com/file2.txt'),
])
// Join async computations
.then(([text1, text2]) => {
assert.equal(text1, 'Content of file1.txt\n');
assert.equal(text2, 'Content of file2.txt\n');
});
httpGet()
is the promisified version of XMLHttpRequest
that we implemented earlier.
.map()
via Promise.all()
Array transformation methods such as .map()
, .filter()
, etc., are made for synchronous computations. For example:
function timesTwoSync(x) {
return 2 * x;
}
const arr = [1, 2, 3];
const result = arr.map(timesTwoSync);
assert.deepEqual(result, [2, 4, 6]);
Is it possible for the callback of .map()
to be a Promise-based function? Yes it is, if you use Promise.all()
to convert an Array of Promises to an Array of (fulfillment) values:
function timesTwoAsync(x) {
return new Promise(resolve => resolve(x * 2));
}
const arr = [1, 2, 3];
const promiseArr = arr.map(timesTwoAsync);
Promise.all(promiseArr)
.then(result => {
assert.deepEqual(result, [2, 4, 6]);
});
This following code is a more realistic example: In the section on fork-join, there was an example where we downloaded two resources identified by two fixed URLs. Let’s turn that code fragment into a function that accepts an Array of URLs and downloads the corresponding resources:
function downloadTexts(urls) {
const promisedTexts = urls.map(httpGet);
return Promise.all(promisedTexts);
}
downloadTexts([
'http://example.com/file1.txt',
'http://example.com/file2.txt',
])
.then(texts => {
assert.deepEqual(
texts, [
'Content of file1.txt\n',
'Content of file2.txt\n',
]);
});
Exercise:
Promise.all()
and listing files
exercises/promises/list_files_async_test.mjs
This section gives tips for chaining Promises.
Problem:
// Don’t do this
function foo() {
const promise = asyncFunc();
promise.then(result => {
// ···
});
return promise;
}
Computation starts with the Promise returned by asyncFunc()
. But afterwards, computation continues and another Promise is created, via .then()
. foo()
returns the former Promise, but should return the latter. This is how to fix it:
Problem:
// Don’t do this
asyncFunc1()
.then(result1 => {
return asyncFunc2()
.then(result2 => { // (A)
// ···
});
});
The .then()
in line A is nested. A flat structure would be better:
This is another example of avoidable nesting:
// Don’t do this
asyncFunc1()
.then(result1 => {
if (result1 < 0) {
return asyncFuncA()
.then(resultA => 'Result: ' + resultA);
} else {
return asyncFuncB()
.then(resultB => 'Result: ' + resultB);
}
});
We can once again get a flat structure:
asyncFunc1()
.then(result1 => {
return result1 < 0 ? asyncFuncA() : asyncFuncB();
})
.then(resultAB => {
return 'Result: ' + resultAB;
});
In the following code, we actually benefit from nesting:
db.open()
.then(connection => { // (A)
return connection.select({ name: 'Jane' })
.then(result => { // (B)
// Process result
// Use `connection` to make more queries
})
// ···
.finally(() => {
connection.close(); // (C)
});
})
We are receiving an asynchronous result in line A. In line B, we are nesting, so that we have access to variable connection
inside the callback and in line C.
Problem:
// Don’t do this
class Model {
insertInto(db) {
return new Promise((resolve, reject) => { // (A)
db.insert(this.fields)
.then(resultCode => {
this.notifyObservers({event: 'created', model: this});
resolve(resultCode);
}).catch(err => {
reject(err);
})
});
}
// ···
}
In line A, we are creating a Promise to deliver the result of db.insert()
. That is unnecessarily verbose and can be simplified:
class Model {
insertInto(db) {
return db.insert(this.fields)
.then(resultCode => {
this.notifyObservers({event: 'created', model: this});
return resultCode;
});
}
// ···
}
The key idea is that we don’t need to create a Promise; we can return the result of the .then()
call. An additional benefit is that we don’t need to catch and re-reject the failure of db.insert()
. We simply pass its rejection on, to the caller of .insertInto()
.
“Exploring ES6” has a section that shows a very simple implementation of Promises. That may be helpful if you want a deeper understanding of how Promises work.