DEV Community

Samuel Rouse
Samuel Rouse

Posted on

Promises Resolved

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'
Enter fullscreen mode Exit fullscreen mode

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'
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 ]
Enter fullscreen mode Exit fullscreen mode

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.'
Enter fullscreen mode Exit fullscreen mode

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.'
Enter fullscreen mode Exit fullscreen mode

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!

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (0)

Cloudinary image

Optimize, customize, deliver, manage and analyze your images.

Remove background in all your web images at the same time, use outpainting to expand images with matching content, remove objects via open-set object detection and fill, recolor, crop, resize... Discover these and hundreds more ways to manage your web images and videos on a scale.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay