Recommended reading
This chapter builds on the previous chapter with background on asynchronous programming in JavaScript.
Promises are a technique for delivering results asynchronously. Instead of directly returning a result, a Promise-based function returns a Promise: a container object that is initially empty. If and when the function is eventually done, it puts either a result or an error into the Promise.
The following code shows how the Promise-based function addAsync()
is used. We’ll see how that function is implemented soon.
const promise = addAsync(3, 4);
promise.then((result) => { // success
assert.equal(result, 7);
})
.catch((error) => { // failure
assert.fail(error);
})
;
To access what’s inside the Promise (if and when there is something inside it), we register callbacks:
.then()
registers callbacks that are called with the result (if and when there is one).
.catch()
registers callbacks that are called with the error (if and when there is one).
This aspect of Promises is similar to the event pattern.
Promises differ from events in two ways:
They deliver at most one result and are optimized for that use case:
If we register a .then()
callback when a Promise is still empty, it is notified if and when the Promise receives a result. If we register a .catch()
callback when a Promise is still empty, it is notified if and when the Promise receives an error.
Once a Promise receives a result or an error, that value is cached. Thus, if we register a callback after that happens, it gets the cached value (depending on whether it is eligible to receive it). That means there is no risk of registering a callback too late.
The first value received by a Promise permanently settles that Promise: Values it receives later are ignored.
We can chain the Promise methods .then()
and .catch()
because they both return Promises. That helps with sequentially invoking multiple asynchronous functions. More on that later.
This is an implementation of a Promise-based function that adds two numbers x
and y
:
function addAsync(x, y) {
return new Promise( // (A)
(resolve, reject) => { // (B)
if (x === undefined || y === undefined) {
reject(new Error('Must provide two parameters'));
} else {
resolve(x + y);
}
}
);
}
addAsync()
immediately creates and returns a Promise by new-invoking the Promise
constructor (line A). It can only change the state of the new Promise from inside the callback (line B) that it passes to the constructor:
resolve
is a function that puts a result into the Promise (in case of success).
reject
is a function that puts an error into the Promise (in case of failure).
One we have invoked either these functions, subsequent invocations of them have no effect.
The Promise constructor uses the revealing constructor pattern:
const promise = new Promise(
(resolve, reject) => {
// ···
}
);
Quoting Domenic Denicola, one of the people behind JavaScript’s Promise API:
I call this the revealing constructor pattern because the
Promise
constructor is revealing its internal capabilities, but only to the code that constructs the promise in question. The ability to resolve or reject the promise is only revealed to the constructing code, and is crucially not revealed to anyone using the promise. So if we hand offp
to another consumer, say
doThingsWith(p);
then we can be sure that this consumer cannot mess with any of the internals that were revealed to us by the constructor. This is as opposed to, for example, putting
resolve
andreject
methods onp
, which anyone could call.
Figure 43.1 depicts the three states a Promise can be in.
Figure 43.1: A Promise is initially in the state “pending”. It can later transition to either the state “fulfilled” or the state “rejected” (but it may never do so). If a Promise is in a final (non-pending) state, it is called settled.
This is an example of a Promise that is never settled and forever pending:
new Promise(() => {})
A Promise can only be fulfilled with a non-Promise value. In contrast, we can resolve a Promise with either a non-Promise value or a Promise. If the callback of new Promise()
calls resolve(x)
then it depends on x
what happens to the newly created Promise p
:
x
is a non-Promise value then p
is fulfilled with x
.
x
is a Promise, then p
adopts the state of x
(which basically results in x
replacing p
). In other words:
x
is pending, p
is pending.
x
is fulfilled, p
is fulfilled.
x
is rejected, p
is rejected.
In other words: Resolving only determines the fate of a Promise; it may or may not fulfill it. This behavior helps with chaining Promise methods. More on that later.
Promise.resolve()
and Promise.reject()
If x
is a non-Promise value then Promise.resolve(x)
creates a Promise that is fulfilled with that value:
Promise.resolve(123)
.then((x) => {
assert.equal(x, 123);
});
If the argument is already a Promise, it is returned unchanged:
const abcPromise = Promise.resolve('abc');
assert.equal(
Promise.resolve(abcPromise), abcPromise
);
Promise.reject(err)
accepts a value err
(that is normally not a Promise) and returns a Promise that is rejected with it:
const myError = new Error('My error!');
Promise.reject(myError)
.catch((err) => {
assert.equal(err, myError);
});
Why is that useful?
Promise.resolve()
to convert a value that may or may not be a Promise to a value that is guaranteed to be a Promise.
Promise.resolve()
and Promise.reject()
– as demonstrated by the example below.
function convertToNumber(stringOrNumber) {
if (typeof stringOrNumber === 'number') {
return Promise.resolve(stringOrNumber);
} else if (typeof stringOrNumber === 'string') {
return stringToNumberAsync(stringOrNumber);
} else {
return Promise.reject(new TypeError());
}
}
.then()
callbacks.then()
registers callbacks for Promise fulfillments. It also returns a new Promise. Doing so enables method chaining: We can invoke .then()
and .catch()
on the result and keep the asynchronous computation going.
How the Promise returned by .then()
is resolved, depends on what happens inside its callback. Let’s look at three common cases.
.then()
callbackFirst, 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)
});
.then()
callbackSecond, the callback can return a Promise q
(line A). Consequently, the Promise p
returned by .then()
is resolved with q
. In other words: p
is effectively replaced by q
.
Promise.resolve('abc')
.then((str) => {
return Promise.resolve(123); // (A)
})
.then((num) => {
assert.equal(num, 123);
});
Why is that useful? We 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) => {
/*···*/
});
});
.then()
callbackThird, 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 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 returning Promises 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:
asyncFunc1().then(asyncFunc2)
asyncFunc1
and asyncFunc2
sequentially.
syncFunc1(); syncFunc2()
syncFunc1
and syncFunc2
sequentially.
We can also add .catch()
into the mix and let it handle multiple error sources at the same time:
asyncFunc1()
.then((result1) => {
// ···
return asyncFunc2();
})
.then((result2) => {
// ···
})
.catch((error) => {
// Failure: handle errors of asyncFunc1(), asyncFunc2()
// and any (sync) exceptions thrown in previous callbacks
});
Promise.try()
: starting a Promise chain ES2025Where the Promise method .then(cb)
continues a Promise chain, Promise.try(cb)
starts a Promise chain – while treating the callback cb
similarly:
cb
.
cb
throws an exception, Promise.try()
returns a rejection with that exception.
cb
returns a value, Promise.try()
resolves that value into a Promise (without nesting if the value already is a Promise).
Promise.try()
: starting a Promise chain with code that is not purely asynchronousWe need Promise.try()
to start a Promise chain with code that is a mix of synchronous and asynchronous functionality:
.then()
are good tools for dealing with mixed code.
Let’s look at an example:
function computeAsync() {
return Promise.try(() => {
const value = syncFuncMightThrow(); // (A)
return asyncFunc(value); // (B)
});
}
We have a mix of synchronous functionality (line A) and asynchronous functionality (line B).
Why wrap the code inside the callback at all? It helps if the synchronous function we call in line A throws an exception: Then Promise.try()
catches that exception and converts it into a rejected Promise for us. Therefore, the previous code is mostly equivalent to:
function computeAsync() {
try {
const value = syncFuncMightThrow();
return asyncFunc(value);
} catch (err) {
return Promise.reject(err);
}
}
Promise.try()
is only needed if we work directly with Promises. Async functions (which are explained in the next chapter) already provide good support for dealing with a mix of sync and async code (anywhere).
Promise.try()
This following code is an alternative to Promise.try()
:
function countPlusOneAsync() {
return Promise.resolve().then(
() => countSyncOrAsync() // (A)
)
.then((result) => {
return result + 1;
});
}
Promise.resolve()
creates a Promise that is fulfilled with undefined
. That result does not matter to us. What does matter is that we have just started a Promise chain and can put the code to try into the callback in line A.
The main downside compared to Promise.try()
is that this pattern executes the code in line A on the next tick (and not immediately).
Promise.prototype.finally()
ES2018The Promise method .finally()
is often used as follows:
somePromise
.then((result) => {
// ···
})
.catch((error) => {
// ···
})
.finally(() => {
// ···
})
;
The .finally()
callback is always executed – independently of somePromise
and the values returned by .then()
and/or .catch()
. In contrast:
.then()
callback is only executed if somePromise
is fulfilled.
.catch()
callback is only executed if:
somePromise
is rejected,
.then()
callback returns a rejected Promise,
.then()
callback throws an exception.
If the callback returns a non-Promise value or a fulfilled Promise, .finally()
ignores that result and simply passes on the settlement that existed before it was called:
Promise.resolve(123)
.finally(() => {})
.then((result) => {
assert.equal(result, 123);
});
Promise.reject('error')
.finally(() => {})
.catch((error) => {
assert.equal(error, 'error');
});
If however, the .finally()
callback throws an exception or returns a rejected Promise, the Promise returned by .finally()
is rejected:
Promise.reject('error (previously)')
.finally(() => {
throw 'error (finally)';
})
.catch((error) => {
assert.equal(error, 'error (finally)');
});
Promise.reject('error (previously)')
.finally(() => {
return Promise.reject('error (finally)');
})
.catch((error) => {
assert.equal(error, 'error (finally)');
});
.finally()
: cleaning upOne common use case for .finally()
is similar to a common use case of the synchronous finally
clause: cleaning up after you are done with a resource. That should always happen, regardless of whether everything went smoothly or there was an error – for example:
let connection;
db.open()
.then((conn) => {
connection = conn;
return connection.select({ name: 'Jane' });
})
.then((result) => {
// Process result
// Use `connection` to make more queries
})
// ···
.catch((error) => {
// handle errors
})
.finally(() => {
connection.close();
});
.finally()
: doing something first after any kind of settlementWe can also use .finally()
before both .then()
and .catch()
. Then what we do in the .finally()
callback is always executed before the other two callbacks. As an example, consider the following function handleAsyncResult()
:
function handleAsyncResult(promise) {
return promise
.finally(() => {
console.log('finally');
})
.then((result) => {
console.log('then ' + result);
})
.catch((error) => {
console.log('catch ' + error);
})
;
}
This is what happens with a fulfilled Promise:
handleAsyncResult(Promise.resolve('fulfilled'));
Output:
finally
then fulfilled
This is what happens with a rejected Promise:
handleAsyncResult(Promise.reject('rejected'));
Output:
finally
catch rejected
Promise.withResolvers()
ES2024The most common way of creating and resolving a Promise is via the Promise
constructor:
new Promise(
(resolve, reject) => { ··· }
);
One limitation of creating Promises like that is that the settlement functions resolve
and reject
are meant to only be used inside the callback. Sometimes we want to use them outside of it. That’s when the following static factory method is useful:
const { promise, resolve, reject } = Promise.withResolvers();
This is what using that factory method looks like:
{
const { promise, resolve, reject } = Promise.withResolvers();
resolve('fulfilled');
assert.equal(
await promise,
'fulfilled'
);
}
{
const { promise, resolve, reject } = Promise.withResolvers();
reject('rejected');
try {
await promise;
} catch (err) {
assert.equal(err, 'rejected');
}
}
Why the name
withResolvers
? Why not, e.g., withSettlers
?
resolve()
may not settle promise
– it only resolves it.
resolve
and reject
.
Exercise: Implementing an asynchronous queue via a linked list whose elements are Promises
exercises/promises/promise-queue_test.mjs
We can implement Promise.withResolvers()
as follows:
function promiseWithResolvers() {
let resolve;
let reject;
const promise = new Promise(
(res, rej) => {
// Executed synchronously!
resolve = res;
reject = rej;
});
return {promise, resolve, reject};
}
class OneElementQueue {
#promise = null;
#resolve = null;
constructor() {
const { promise, resolve } = Promise.withResolvers();
this.#promise = promise;
this.#resolve = resolve;
}
get() {
return this.#promise;
}
put(value) {
this.#resolve(value);
}
}
{ // Putting before getting
const queue = new OneElementQueue();
queue.put('one');
assert.equal(
await queue.get(),
'one'
);
}
{ // Getting before putting
const queue = new OneElementQueue();
setTimeout(
// Runs after `await` pauses the current execution context
() => queue.put('two'),
0
);
assert.equal(
await queue.get(),
'two'
);
}
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 we use callbacks for asynchronicity, exceptions are normally not handled for us; we have to do it ourselves.
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:
{
"first": "Jane",
"last": "Doe"
}
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 'node: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 readFile()
from node:fs/promises
, the Promise-based version of fs.readFile()
:
import {readFile} from 'node:fs/promises';
readFile('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 readFile()
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 readFile()
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 to 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
Most JavaScript platforms support Fetch, a 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 we can use fetch()
as follows:
fetch('http://example.com/textfile.txt')
.then(response => response.text())
.then((text) => {
assert.equal(text, 'Content of textfile.txt');
});
fetch()
is also used here: “Example: .map()
with an async function as a callback” (§44.3.3).
Exercise: Using the fetch API
exercises/promises/fetch_json_test.mjs
Tip for implementing functions and methods:
Don’t mix (asynchronous) rejections and (synchronous) exceptions.
This makes our synchronous and asynchronous code more predictable and simpler because we 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 we 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 always return results asynchronously; we can be sure that there is never a synchronous return. This kind of predictability makes code easier to work with.
More information on this approach
“Designing APIs for Asynchrony” by Isaac Z. Schlueter
The combinator pattern is a pattern in functional programming for building structures. It is based on two kinds of functions:
When it comes to JavaScript Promises:
Primitive functions include: Promise.resolve()
, Promise.reject()
Combinators include: Promise.all()
, Promise.race()
, Promise.any()
, Promise.allSettled()
. In each of these cases:
Next, we’ll take a closer look at the mentioned Promise combinators.
Promise.all()
This is the type signature of Promise.all()
:
Promise.all<T>(promises: Iterable<Promise<T>>): Promise<Array<T>>
Promise.all()
returns a Promise which is:
promises
are fulfilled.
promises
.
This is a quick demo of the output Promise being fulfilled:
const promises = [
Promise.resolve('result a'),
Promise.resolve('result b'),
Promise.resolve('result c'),
];
Promise.all(promises)
.then((arr) => assert.deepEqual(
arr, ['result a', 'result b', 'result c']
));
The following example demonstrates what happens if at least one of the input Promises is rejected:
const promises = [
Promise.resolve('result a'),
Promise.resolve('result b'),
Promise.reject('ERROR'),
];
Promise.all(promises)
.catch((err) => assert.equal(
err, 'ERROR'
));
Figure 43.2 illustrates how Promise.all()
works.
Figure 43.2: The Promise combinator Promise.all()
.
.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]);
What happens if the callback of .map()
is a Promise-based function (a function that maps normal values to Promises)? Then the result of .map()
is an Array of Promises. Alas, that is not data that normal code can work with. Thankfully, we can fix that via Promise.all()
: It converts an Array of Promises into a Promise that is fulfilled with an Array of normal 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]);
});
.map()
exampleNext, we’ll use .map()
and Promise.all()
to downlooad text files from the web. For that, we need the following tool function:
function downloadText(url) {
return fetch(url)
.then((response) => { // (A)
if (!response.ok) { // (B)
throw new Error(response.statusText);
}
return response.text(); // (C)
});
}
downloadText()
uses the Promise-based fetch API to download a text file as a string:
response
(line A).
response.ok
(line B) checks if there were errors such as “file not found”.
.text()
(line C) to retrieve the content of the file as a string.
In the following example, we download two text files:
const urls = [
'http://example.com/first.txt',
'http://example.com/second.txt',
];
const promises = urls.map(
url => downloadText(url));
Promise.all(promises)
.then(
(arr) => assert.deepEqual(
arr, ['First!', 'Second!']
));
Promise.all()
This is a simplified implementation of Promise.all()
(e.g., it performs no safety checks):
function all(iterable) {
return new Promise((resolve, reject) => {
let elementCount = 0;
let result;
let index = 0;
for (const promise of iterable) {
// Preserve the current value of `index`
const currentIndex = index;
promise.then(
(value) => {
result[currentIndex] = value;
elementCount++;
if (elementCount === result.length) {
resolve(result); // (A)
}
},
(err) => {
reject(err); // (B)
});
index++;
}
if (index === 0) {
// Resolution is normally done in line A
resolve([]);
return;
}
// Now we know how many Promises there are in `iterable`.
// We can wait until now with initializing `result` because
// the callbacks of .then() are executed asynchronously.
result = new Array(index);
});
}
The two main locations where the result Promise is settled are line A and line B. After one of them settled, the other can’t change the settlement value anymore because a Promise can only be settled once.
Promise.race()
This is the type signature of Promise.race()
:
Promise.race<T>(promises: Iterable<Promise<T>>): Promise<T>
Promise.race()
returns a Promise q
which is settled as soon as the first Promise p
among promises
is settled. q
has the same settlement value as p
.
In the following demo, the settlement of the fulfilled Promise (line A) happens before the settlement of the rejected Promise (line B). Therefore, the result is also fulfilled (line C).
const promises = [
new Promise((resolve, reject) =>
setTimeout(() => resolve('result'), 100)), // (A)
new Promise((resolve, reject) =>
setTimeout(() => reject('ERROR'), 200)), // (B)
];
Promise.race(promises)
.then((result) => assert.equal( // (C)
result, 'result'));
In the next demo, the rejection happens first:
const promises = [
new Promise((resolve, reject) =>
setTimeout(() => resolve('result'), 200)),
new Promise((resolve, reject) =>
setTimeout(() => reject('ERROR'), 100)),
];
Promise.race(promises)
.then(
(result) => assert.fail(),
(err) => assert.equal(
err, 'ERROR'));
Note that the Promise returned by Promise.race()
is settled as soon as the first among its input Promises is settled. That means that the result of Promise.race([])
is never settled.
Figure 43.3 illustrates how Promise.race()
works.
Figure 43.3: The Promise combinator Promise.race()
.
Promise.race()
to time out a PromiseIn this section, we are going to use Promise.race()
to time out Promises. We will be using the following helper functions:
/**
* Returns a Promise that is resolved with `value`
* after `ms` milliseconds.
*/
function resolveAfter(ms, value=undefined) {
return new Promise((resolve, _reject) => {
setTimeout(() => resolve(value), ms);
});
}
/**
* Returns a Promise that is rejected with `reason`
* after `ms` milliseconds.
*/
function rejectAfter(ms, reason=undefined) {
return new Promise((_resolve, reject) => {
setTimeout(() => reject(reason), ms);
});
}
This function times out a Promise:
function timeout(timeoutInMs, promise) {
return Promise.race([
promise,
rejectAfter(timeoutInMs,
new Error('Operation timed out')
),
]);
}
timeout()
returns a Promise whose settlement is the same as the one of whichever Promise settles first among the following two:
promise
timeoutInMs
milliseconds
To produce the second Promise, timeout()
uses the fact that resolving a pending Promise with a rejected Promise leads to the former being rejected.
Let’s see timeout()
in action. Here, the input Promise is fulfilled before the timeout. Therefore, the output Promise is fulfilled.
timeout(200, resolveAfter(100, 'Result!'))
.then(result => assert.equal(result, 'Result!'));
Here, the timeout happens before the input Promise is fulfilled. Therefore, the output Promise is rejected.
timeout(100, resolveAfter(200, 'Result!'))
.catch(err => assert.deepEqual(err, new Error('Operation timed out')));
It is important to understand what “timing out a Promise” really means:
That is, timing out only prevents the input Promise from affecting the output (since a Promise can only be settled once). But it does not stop the asynchronous operation that produced the input Promise.
Promise.race()
This is a simplified implementation of Promise.race()
(e.g., it performs no safety checks):
function race(iterable) {
return new Promise((resolve, reject) => {
for (const promise of iterable) {
promise.then(
(value) => {
resolve(value); // (A)
},
(err) => {
reject(err); // (B)
});
}
});
}
The result Promise is settled in either line A or line B. Once it is, the settlement value can’t be changed anymore.
Promise.any()
ES2021
This is the type signature of Promise.any()
:
Promise.any<T>(promises: Iterable<Promise<T>>): Promise<T>
Promise.any()
returns a Promise p
. How it is settled, depends on the parameter promises
(which refers to an iterable over Promises):
p
is resolved with that Promise.
p
is rejected with an instance of AggregateError
that contains all rejection values.
Figure 43.4 illustrates how Promise.any()
works.
Figure 43.4: The Promise combinator Promise.any()
.
AggregateError
ES2021This is the type signature of AggregateError
(a subclass of Error
):
class AggregateError extends Error {
// Instance properties (complementing the ones of Error)
errors: Array<any>;
constructor(
errors: Iterable<any>,
message: string = '',
options?: ErrorOptions // ES2022
);
}
interface ErrorOptions {
cause?: any; // ES2022
}
This is what happens if one Promise is fulfilled:
const promises = [
Promise.reject('ERROR A'),
Promise.reject('ERROR B'),
Promise.resolve('result'),
];
Promise.any(promises)
.then((result) => assert.equal(
result, 'result'
));
This is what happens if all Promises are rejected:
const promises = [
Promise.reject('ERROR A'),
Promise.reject('ERROR B'),
Promise.reject('ERROR C'),
];
Promise.any(promises)
.catch((aggregateError) => assert.deepEqual(
aggregateError.errors,
['ERROR A', 'ERROR B', 'ERROR C']
));
Promise.any()
vs. Promise.all()
There are two ways in which Promise.any()
and Promise.all()
can be compared:
Promise.all()
: First input rejection rejects the result Promise or its fulfillment value is an Array with input fulfillment values.
Promise.any()
: First input fulfillment fulfills the result Promise or its rejection value is an Array with input rejection values (inside an error object).
Promise.all()
is interested in all fulfillments. The opposite case (at least one rejection) leads to a rejection.
Promise.any()
is interested in the first fulfillment. The opposite case (only rejections) leads to a rejection.
Promise.any()
vs. Promise.race()
Promise.any()
and Promise.race()
are also related, but interested in different things:
Promise.race()
is interested in settlements. The Promise which is settled first, “wins”. In other words: We want to know about the asynchronous computation that terminates first.
Promise.any()
is interested in fulfillments. The Promise which is fulfilled first, “wins”. In other words: We want to know about the asynchronous computation that succeeds first.
The main – relatively rare – use case for .race()
is timing out Promises. The use cases for .any()
are broader. We’ll look at them next.
Promise.any()
We use Promise.any()
if we have multiple asynchronous computations and we are only interested in the first successful one. In a way, we let the computations compete with each other and use whichever one is fastest.
The following code demonstrates what that looks like when downloading resources:
const resource = await Promise.any([
fetch('http://example.com/first.txt')
.then(response => response.text()),
fetch('http://example.com/second.txt')
.then(response => response.text()),
]);
The same pattern enables us to use whichever module downloads more quickly:
const mylib = await Promise.any([
import('https://primary.example.com/mylib'),
import('https://secondary.example.com/mylib'),
]);
For comparison, this is the code we’d use if the secondary server is only a fallback – in case the primary server fails:
let mylib;
try {
mylib = await import('https://primary.example.com/mylib');
} catch {
mylib = await import('https://secondary.example.com/mylib');
}
Promise.any()
?A simple implementation of Promise.any()
is basically a mirror version of the implementation of Promise.all()
.
Promise.allSettled()
ES2020
This time, the type signatures are a little more complicated. Feel free to skip ahead to the first demo, which should be easier to understand.
This is the type signature of Promise.allSettled()
:
Promise.allSettled<T>(promises: Iterable<Promise<T>>)
: Promise<Array<SettlementObject<T>>>
It returns a Promise for an Array whose elements have the following type signature:
type SettlementObject<T> = FulfillmentObject<T> | RejectionObject;
interface FulfillmentObject<T> {
status: 'fulfilled';
value: T;
}
interface RejectionObject {
status: 'rejected';
reason: unknown;
}
Promise.allSettled()
returns a Promise out
. Once all promises
are settled, out
is fulfilled with an Array. Each element e
of that Array corresponds to one Promise p
of promises
:
If p
is fulfilled with the fulfillment value v
, then e
is
{ status: 'fulfilled', value: v }
If p
is rejected with the rejection value r
, then e
is
{ status: 'rejected', reason: r }
Unless there is an error when iterating over promises
, the output Promise out
is never rejected.
Figure 43.5 illustrates how Promise.allSettled()
works.
Figure 43.5: The Promise combinator Promise.allSettled()
.
Promise.allSettled()
This is a quick first demo of how Promise.allSettled()
works:
Promise.allSettled([
Promise.resolve('value'),
Promise.reject('ERROR'),
])
.then(arr => assert.deepEqual(arr, [
{ status: 'fulfilled', value: 'value' },
{ status: 'rejected', reason: 'ERROR' },
]));
Promise.allSettled()
The next example is similar to the .map()
plus Promise.all()
example (from which we are borrowing the function downloadText()
): We are downloading multiple text files whose URLs are stored in an Array. However, this time, we don’t want to stop when there is an error, we want to keep going. Promise.allSettled()
allows us to do that:
function downloadText(url) {
return fetch(url)
.then((response) => {
if (!response.ok) {
throw new Error(response.statusText);
}
return response.text();
});
}
const urls = [
'http://example.com/exists.txt',
'http://example.com/missing.txt',
];
const result = Promise.allSettled(
urls.map(url => downloadText(url))
);
result.then(
(arr) => {
assert.deepEqual(
arr,
[
{
status: 'fulfilled',
value: 'Hello!',
},
{
status: 'rejected',
reason: new Error('Not Found'),
},
]
)
}
);
Promise.allSettled()
This is a simplified implementation of Promise.allSettled()
(e.g., it performs no safety checks):
function allSettled(iterable) {
return new Promise((resolve, reject) => {
let elementCount = 0;
let result;
function addElementToResult(i, elem) {
result[i] = elem;
elementCount++;
if (elementCount === result.length) {
resolve(result);
}
}
let index = 0;
for (const promise of iterable) {
// Capture the current value of `index`
const currentIndex = index;
promise.then(
(value) => addElementToResult(
currentIndex, {
status: 'fulfilled',
value
}
),
(reason) => addElementToResult(
currentIndex, {
status: 'rejected',
reason
}
)
);
index++;
}
if (index === 0) {
// Resolution is normally triggered by addElementToResult()
resolve([]);
return;
}
// Now we know how many Promises there are in `iterable`.
// We can wait until now with initializing `result` because
// the callbacks of .then() are executed asynchronously.
result = new Array(index);
});
}
Exercises: Promise combinator methods
Getting the highest fulfillment value of an iterable over Promises: exercises/promises/get-highest-fulfillment_test.mjs
Implementing Promise.anySettled()
: exercises/promises/promise-any-settled_test.mjs
For a Promise combinator, short-circuiting means that the output Promise is settled early – before all input Promises are settled. The following combinators short-circuit:
Promise.all()
: The output Promise is rejected as soon as one input Promise is rejected.
Promise.race()
: The output Promise is settled as soon as one input Promise is settled.
Promise.any()
: The output Promise is fulfilled as soon as one input Promise is fulfilled.
Once again, settling early does not mean that the operations behind the ignored Promises are stopped. It just means that their settlements are ignored.
Promise.all()
(advanced)Consider 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.
Promise.all()
helps execute Promise-based functions more concurrently:
Promise.all([asyncFunc1(), asyncFunc2()])
.then((arr) => {
assert.deepEqual(arr, ['one', 'two']);
});
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”. Let’s revisit an example that we have encountered previously:
Promise.all([
// (A) fork
downloadText('http://example.com/first.txt'),
downloadText('http://example.com/second.txt'),
])
// (B) join
.then(
(arr) => assert.deepEqual(
arr, ['First!', 'Second!']
));
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 afterward, 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:
function foo() {
const promise = asyncFunc();
return promise.then((result) => {
// ···
});
}
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:
asyncFunc1()
.then((result1) => {
return asyncFunc2();
})
.then((result2) => {
// ···
});
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()
.
When Promises were added to JavaScript’s standard library in ES6 (in 2015), several Promise libraries were popular and widely used. To make those libraries interoperable with the built-in API, TC39 defined a minimal interface for Promise-like objects that is compatible with most of those libraries. As often as possible, the API does not require objects to be Promises – it’s enough if they are Promise-like. If necessary, the API transparently converts Promise-like objects to API Promises.
So what minimal interface describes the essence of Promises? It only needs a method .then()
that lets us register callbacks:
const promiseLikeObject = {
then(onFulfilled, onRejected) {
// ···
},
};
This is a simplified version of TypeScript’s type for Promise-like objects:
interface PromiseLike<T> {
then<TResult1, TResult2>(
onFulfilled?: (value: T) => TResult1 | PromiseLike<TResult1>,
onRejected?: (reason: any) => TResult2 | PromiseLike<TResult2>
): PromiseLike<TResult1 | TResult2>;
}
This interface is sufficient because .catch()
is actually just a convenient way of invoking .then()
(whose second parameter we have ignored previously) – the following two invocations are equivalent:
promise.catch(onRejected)
promise.then(undefined, onRejected)
Because Promise-like objects only have a method .then()
, they are also called thenables.
The following object is a fulfilled thenable:
const fulfilledThenable = {
then(onFulfilled, onRejected) {
onFulfilled('Success!');
},
};
If we pass the thenable to Promise.resolve()
, it converts it to a Promise:
const promise = Promise.resolve(fulfilledThenable);
assert.equal(
promise instanceof Promise, true
);
Returning the thenable from a callback is equivalent to returning a Promise:
Promise.resolve()
.then(() => fulfilledThenable)
.then((value) => {
assert.equal(value, 'Success!');
});
We can also resolve a new Promise with a thenable:
new Promise((resolve) => {
resolve(fulfilledThenable);
}).then((value) => {
assert.equal(value, 'Success!');
});
The following code demonstrates a rejected thenable:
const rejectedThenable = {
then(onFulfilled, onRejected) {
onRejected('Error!');
},
};
Promise.resolve(rejectedThenable)
.catch((reason) => {
assert.equal(reason, 'Error!');
});
Promise.resolve()
.then(() => rejectedThenable)
.catch((reason) => {
assert.equal(reason, 'Error!');
});
new Promise((resolve) => {
resolve(rejectedThenable);
}).catch((reason) => {
assert.equal(reason, 'Error!');
});
Promise
new Promise()
new Promise(executor)
ES6
new Promise<T>(
executor: (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: any) => void
) => void
): Promise<T>
This constructor creates a new Promise. It passes functions to its callback with which that Promise can be resolved or rejected:
// Create a Promise and resolve it
new Promise((resolve, reject) => {
resolve('Result');
}).then((value) => {
assert.equal(value, 'Result');
});
// Create a Promise and reject it
new Promise((resolve, reject) => {
reject('Error');
}).catch((reason) => {
assert.equal(reason, 'Error');
});
Promise.*
: creating PromisesPromise.withResolvers()
ES2024
Promise.withResolvers<T>(): PromiseWithResolvers<T>;
interface PromiseWithResolvers<T> {
promise: Promise<T>;
resolve: (value: T | PromiseLike<T>) => void;
reject: (reason?: any) => void;
}
This method creates a Promise and returns an object that contains that Promise plus functions for resolving or rejecting it.
Promise.resolve(value?)
ES6
Creates a Promise, resolves it with value
and returns it:
Promise.resolve('Yes')
.then((value) => {
assert.equal(value, 'Yes');
});
Promise.reject(reason?)
ES6
Creates a Promise, rejects it with value
and returns it:
Promise.reject('No')
.catch((reason) => {
assert.equal(reason, 'No');
});
Promise.*
: miscellaneous functionalityPromise.try(callback, ...args)
ES2025
Creates a Promise by treating callback
as if it were a .then()
callback:
callback
with zero or more arguments.
callback
throws an exception, Promise.try()
turns it into a rejected Promise and returns it.
callback
returns a value, Promise.try()
resolves it to a Promise and returns it.
The use case for this method is starting a Promise chain with code that is not purely asynchronous – e.g.:
function computeAsync() {
return Promise.try(() => {
const value = syncFuncMightThrow();
return asyncFunc(value);
});
}
Promise.*
: Promise combinatorsGlossary:
These are the Promise combinators:
Promise.all(promises)
ES6
Promise.all<T>(
promises: Iterable<Promise<T>>
): Promise<Array<T>>
P
: if all input Promises are fulfilled.
P
: if one input Promise is rejected.
Promise.race(promises)
ES6
Promise.race<T>(
promises: Iterable<Promise<T>>
): Promise<T>
P
: if the first input Promise is settled.
Promise.any(promises)
ES2021
Promise.any<T>(
promises: Iterable<Promise<T>>
): Promise<T>
P
: if one input Promise is fulfilled.
P
: if all input Promises are rejected.
AggregateError
that contains the rejection values of the input Promises.
This is the type signature of AggregateError
(a few members were omitted):
class AggregateError {
constructor(errors: Iterable<any>, message: string);
get errors(): Array<any>;
get message(): string;
}
Promise.allSettled(promises)
ES2020
Promise.allSettled<T>(
promises: Iterable<Promise<T>>
): Promise<Array<SettlementObject<T>>>
P
: if all input Promise are settled.
P
: if there is an error when iterating over the input Promises.
This is the type signature of SettlementObject
:
type SettlementObject<T> = FulfillmentObject<T> | RejectionObject;
interface FulfillmentObject<T> {
status: 'fulfilled';
value: T;
}
interface RejectionObject {
status: 'rejected';
reason: unknown;
}
Promise.prototype.*
Promise.prototype.then(onFulfilled?, onRejected?)
ES6
interface Promise<T> {
then<TResult1, TResult2>(
onFulfilled?: (value: T) => TResult1 | PromiseLike<TResult1>,
onRejected?: (reason: any) => TResult2 | PromiseLike<TResult2>
): Promise<TResult1 | TResult2>;
}
Registers callbacks for the fulfillment value and/or the rejection value of a Promise. Usually, only the first parameter onFulfilled
is used. .catch()
provides a more self-descriptive alternative to using the second parameter onRejected
.
Promise.resolve('Yes')
.then((value) => {
assert.equal(value, 'Yes');
});
Promise.prototype.catch(onRejected)
ES6
interface Promise<T> {
catch<TResult>(
onrejected?: (reason: any) => TResult2 | PromiseLike<TResult2>
): Promise<T | TResult>;
}
Registers a callback for the rejection value of a Promise. A more self-descriptive alternative to using .then()
for that purpose – the following two invocations are equivalent:
promise.catch(onRejected)
promise.then(undefined, onRejected)
Example:
Promise.reject('No')
.catch((reason) => {
assert.equal(reason, 'No');
});
Promise.prototype.finally(onFinally)
ES2018
interface Promise<T> {
// Returning a rejected Promise from onFinally does have an effect!
finally(onFinally?: () => void);
}
Often used as follows:
somePromise
.then((result) => {
// ···
})
.catch((error) => {
// ···
})
.finally(() => {
// ···
})
;
The .finally()
callback is always executed – independently of somePromise
and the values returned by .then()
and/or .catch()
. The callback only has an effect if it returns a rejected Promise or throws an exception. Then the final Promise is rejected with the rejection value or the exception.