When you start integrating promises into your workflow, it can seem like you need to make everything into a promise, but that's not the case.
Never Break The Chain
I can still hear you saying you would never break the chain.
— Fleetwood Mac
Once we move into an asynchronous workflow we shouldn't leave it. Attempting to access an asynchronously-set value from outside is one of the common mistakes when starting to work with asynchronous code, whether it is promises, async/await, or event listeners.
Functions in a promise chain are placed in a microtask queue, so the chain will always execute after any remaining synchronous code. This happens even if the promise itself is resolved synchronously like Promise.resolve()
or the special case of Promise.all([]).
let data = 'one';
// The function passed to .then() is queued
Promise.resolve().then(() => {
data += ' two';
});
// This happens before the update
console.log(data); // 'one'
The fix is clear: don't break the chain. Staying inside the promise chain helps guarantee the order of operations.
let data = 'one';
Promise.resolve().then(() => {
data += ' two';
}).then(() => {
console.log(data); // 'one two'
});
In fact, storing the data outside the promise chain might not be necessary at all.
Promise.resolve('one')
.then(data => `${data} two`)
.then(console.log);
We store the value in the promise. We update (replace) it, We ensure the updated value is passed along. All of this happens in the promise chain, but in all of these examples, each has just one promise created with Promise.resolve()
. The other functions aren't async. They don't know anything about promises.
Implied Promises
The chain always keeps its promise.
– Me, just now.
One of the great things about Promise chains is that they do the work for us. Once we are in a chain, it doesn't matter if the function returns a promise; being called inside the chain maintains the promise. The only time we need a promise is when we start the chain.
We could make sure our first function returns a promise. But what if we change the order of actions? Maybe everything does need to be a promise?!
No. Just like our examples above, we can start with Promise.resolve()
to ensure we have a promise. Just wrap that first function, if we aren't sure it returns a promise. Or even if we are sure! What if that code changes in the future, or we add something as a step before. Starting with Promise.resolve()
is a guarantee.
Resolved Wrapper
If you invoke Promise.resolve()
with another promise, it will quietly unwrap the original promise and pass it through, whether it's resolved, pending, or rejected.
const resolvedPromise = new Promise((resolve) => resolve('yes'));
const foreverPending = new Promise(() => {});
const rejectedPromise = Promise.reject(new Error('No!'));
// Logs our resolved
Promise.resolve(resolvedPromise)
.then(console.log); // 'yes'
// Never logs anything
Promise.resolve(foreverPending)
.then(() => console.log('resolved'))
.catch(() => console.log('rejected'));
// Unwraps the rejected promise
Promise.resolve(rejectedPromise)
.catch(console.error); // Error: No!
The unwrapping of the promise is absolute. It's even strictly equal, showing that we get the original, not a copy.
const foreverPending = new Promise(() => {});
const unwrappedByResolve = Promise.resolve(foreverPending)
foreverPending === unwrappedByResolve; // true
For those cases where you may have multiple promises, Promise.all()
also accepts both promises and values.
Promise.all([10, Promise.resolve(4)])
.then(console.log); // [ 10, 4 ]
Strength from Resolve
Because Promise.resolve()
creates a promise if we need one and unwraps a promise if it receives one, it guarantees a promise chain. Once we have a chain, we can execute all the functions from inside it and don't have to worry about whether each function returns a promise.
// TODO: Replace with network request.
const getUserData = () => ({
username: 'mySuperUniqueName',
password: '123456',
});
// TODO: Should this list be fetched?
const WEAK_PASSWORDS = [
'password',
'123456',
];
const isUserPasswordWeak = (user) => WEAK_PASSWORDS
.includes(user.password);
const reportPasswordStrength = (isWeak) => {
console.log(isWeak
? 'You should change your password.'
: 'Your password seems fine for now.'
);
};
Promise.resolve(getUserData())
.then(isUserPasswordWeak)
.then(reportPasswordStrength);
// 'You should change your password.'
In this simple demo no function return a promise. But, because we are in a promise chain, it doesn't matter if they did. We can convert getUserData
to be async without affecting the rest of the code! It Just Works™.
The chain acts like program control flow rather than some special handling for async logic, which is how I think of promise chains. We don't have to know if the functions are async or not; they always happen in order.
If we decide to also fetch the list of weak passwords, we can easily support that using Promise.all()
.
// TODO: Replace with network request.
const getUserData = () => ({
username: 'mySuperUniqueName',
password: '123456',
});
// TODO: Replace with network request?
const getWeakPasswords = () => [
'password',
'123456',
];
// Accept both sources as an array, for Promise.all
const isUserPasswordWeak = ([user, weakPasswords]) => {
return weakPasswords.includes(user.password);
};
const reportPasswordStrength = (isWeak) => {
console.log(isWeak
? 'You should change your password.'
: 'This seems fine for now.'
);
};
Promise.all([getUserData(), getWeakPasswords()])
.then(isUserPasswordWeak)
.then(reportPasswordStrength);
// 'You should change your password.'
This is still written without promises for the get
functions, but changing to fetch
or however you load the data doesn't change anything else about the chain.
Conclusion
Try thinking about promises as managing the order of operations and not specifically dealing with asynchronous requests. Separate your operations into individual descriptive functions and build your program flow control, then start swapping in asynchronous versions when you need them.
I hope this helps you think about promise chains differently. Let me know if have another perspective to share!
Top comments (0)