Thinking about Promises
Promises act as values
You can think of a Promise as an eventual value. This value could be a success value or a failure value. It either succeeds with a particular successful result, or it fails with an error. Importantly, it can’t be both.
It either resolves to a success value:
// Succeed with the value 42
Promise.resolve(42);
// Longer version of above
new Promise((resolve, reject) => {
resolve(42);
});
Or it rejects with an error value:
// Fail with the error *out of stock*
Promise.reject(Error("out of stock"));
// Longer version of above
new Promise((resolve, reject) => {
reject(Error("out of stock"));
});
// Alternate version of above
new Promise((resolve, reject) => {
throw Error("out of stock");
});
Once a Promise has receive its value, you can’t changed that value. If it was told it failed, it can’t be later told to succeed instead. And if it succeeded, it can’t later fail.
new Promise((resolve, reject) => {
// Once a promise has been resolved or rejected
resolve(42);
// it can’t then be resolved again
resolve(42); // Invalid!
// or be rejected later
reject(Error("out of stock")); // Invalid!
});
Chaining Promises
A Promise can create a new Promise by calling .then()
on it.
Promise.resolve(42)
.then(value => {
return value * 2;
});
// Longer version of above
Promise.resolve(42)
.then(value => {
return Promise.resolve(value * 2);
});
The callback to .then()
can fail, either by throwing an error, or returning a rejected Promise:
Promise.resolve(42)
.then(value => {
throw Error("out of stock");
});
// Longer version of above
Promise.resolve(42)
.then(value => {
return Promise.reject(Error("out of stock"));
});
// Alternative version of above
// Note the Promises can be created up-front.
const outOfStockError = Promise.reject(Error("out of stock"));
Promise.resolve(42)
.then(value => {
return outOfStockError;
});
If a Promise fails, any derived Promises will also fail.
Promise.resolve(42)
.then(value => {
throw Error("out of stock");
})
.then(value => {
// This will never get called as the previous promise was rejected
});
A Promise chain can be recovered by calling .catch()
and returning another value or Promise.
export const a = Promise.resolve(42)
.then(value => {
throw Error("out of stock");
})
.catch(error => {
return 3;
});
// Same as above
export const b = Promise.resolve(42)
.then(value => {
throw Error("out of stock");
})
.catch(error => {
return Promise.resolve(3);
});
// Same as above
const fallbackPromise = Promise.resolve(3);
export const c = Promise.resolve(42)
.then(value => {
throw Error("out of stock");
})
.catch(error => {
return fallbackPromise;
});
// Same as above
const promiseThatWillFail = Promise.resolve(42).then(value => {
throw Error("out of stock");
});
export const d = promiseThatWillFail.catch(error => {
return fallbackPromise;
});
Promises are eager
Let's compare two code samples. How many times will we see Creating value logged?
const promisedValue = new Promise((resolve, reject) => {
// Will this be logged once or not at all?
console.log("Creating value");
resolve(40 + 2);
});
const promisedValue = new Promise((resolve, reject) => {
// Will this logged three times or once?
console.log("Creating value");
resolve(40 + 2);
});
promisedValue.then(console.log);
promisedValue.then(console.log);
promisedValue.then(console.log);
In both cases, we will see it logged only once. This is because promises are run once and created eagerly.
Listening to a promise using .then()
neither affects nor starts that promise. It has no side-effect on the source promise.
Once a promise has been created, then you may wait to hear its result one time, fifteen times, or not at all, and the original promise will behave the same.
This may seem like a strange limitation, but it simplifies reasoning about promises as they work similar to values.
How values work
If we store a value in a variable, we can feel comfortable knowing that the reading of that variable has absolutely no effect on its underlying value.
const value = 40 + 2;
console.log(value);
console.log(value);
console.log(value);
The value of 42
will be logged three times, but if the logs were removed altogether, the variable’s value won’t be affected and will remain the same. The act of logging had no effect on the source value.
Promises work exactly the same.
We can use this to our advantage, by thinking about promises in the same way we think about values.
Reusing
If data is loaded from an API, we might use fetch()
.
const promisedResponse = fetch('https://swapi.dev/api/people/1/');
We can chain the response to decode the JSON body of the response.
const promisedData = fetch('https://swapi.dev/api/people/1/')
.then(response => response.json());
What happens if we want to use this data again?
const promisedData = fetch('https://swapi.dev/api/people/1/')
.then(response => {
// How many times will we see this logged?
console.log('decoding data');
return response.json();
});
promisedData.then(data => {
// Use data
});
promisedData.then(data => {
// Use data again
});
Here we will see ‘decoding data’ logged once. The fetch()
call returns a Promise, which is chained using .then()
where we decode the underlying JSON body by calling .json()
on the response.
We can continue to think of these as eventual values. Once these values have been cast, they cannot change (technically we could mutate anything as JavaScript gives us free reign but we shouldn’t).
The response from fetch()
is one eventual value. The decoded JSON is another eventual value, and actually has two Promises, one created by the .json()
method, and another wrapping that which was created by .then()
.
const promisedResponse = fetch('https://swapi.dev/api/people/1/');
const promisedData = promisedResponse.then(response => {
const promisedDataInner = response.json();
return promisedDataInner;
});
Failure recovery
const fallbackData = {
name: "Jane Doe",
height: "168",
mass: "67",
};
fetch('https://swapi.dev/api/people/1/')
.then(res => res.data())
.catch(() => fallbackData);
Async Await
The same applies if the code is rewritten to use async await
. The underlying objects are still Promises.
The difference is that async
requires a function, which means the code is run from scratch each time.
Here’s our API call written as a function using Promises:
const apiURL = new URL('https://swapi.dev/api/');
function fetchPerson(id) {
return fetch(new URL(`people/${id}/`, apiURL))
.then(response => {
// How many times will we see this logged?
console.log('decoding data');
return response.json();
});
}
function main() {
fetchPerson('1'); // *Creates* promise
fetchPerson('1'); // *Creates* another promise
// We will see 'decoding data' logged twice,
// as our function is run from scratch twice.
}
main();
And here’s that code rewritten to use async await
.
const apiURL = new URL('https://swapi.dev/api/');
async function fetchPerson(id) {
const response = await fetch(new URL(`people/${id}/`, apiURL));
// How many times will we see this logged?
console.log('decoding data');
// Note: if we return a Promise, then there’s no need to await
return response.json();
}
async function main() {
await fetchPerson('1'); // *Creates* promise
await fetchPerson('1'); // *Creates* another promise
// We will see 'decoding data' logged twice,
// as our function is run from scratch twice.
}
main();
However, if we use the result from our fetchPerson()
function (which we be a Promise), and await
that twice (or more) then since we are running the function only once we will see the ‘decoding data’ message logged only once too.
async function main() {
const promise = fetchPerson('1'); // *Creates* promise
await promise;
await promise;
// We will see 'decoding data' logged only once,
// as our function is run only once.
}
main();
This is conceptually similar to calling .then()
on the promise twice.
function main() {
const promise = fetchPerson('1');
promise.then(data => {
// Do nothing
});
promise.then(data => {
// Do nothing
});
// We will see 'decoding data' logged only once,
// as our function is run only once.
}
main();
As we learned earlier, listening to a promise using .then()
neither affects nor starts that promise — it has no side-effect on that promise. And await
behaves the same — it also has no side-effect on the source promise. It simply waits for its eventual value.