JavaScript Promises: A Guide to Asynchronous Operations

Marwan Zaarab
6 min readNov 18, 2022

--

Promises

  • A Promise represents the eventual success value (or failure reason) of an asynchronous operation that hasn’t completed yet. You can think of it as a proxy for a value not necessarily known at the time it is created.
  • Promises simplify deferred and asynchronous computations by returning an object to which you associate handlers with an asynchronous action’s eventual success value or failure reason.
  • This lets asynchronous methods return values like synchronous methods: instead of immediately returning the final value, the asynchronous method returns a promise to supply the value at some point in the future.

Promise states

  • pending: initial state, neither fulfilled nor rejected.
  • fulfilled: the operation was completed successfully.
  • rejected: the operation, i.e., the action relating to the promise, failed.
  • settled: has fulfilled or rejected.

When should the Promise constructor be used?

  • To construct a new Promise object.
  • To create a promise for legacy API callbacks and asynchronous tasks like setTimeout or XMLHttpRequest.

Difference between Event listeners and Promises

  • A promise can only succeed or fail once. It cannot succeed or fail twice, neither can it switch from success to failure or vice versa. For example:
const promise = new Promise((resolve, reject) => {
resolve("Yay!");
reject("Nevermind");
resolve("Yay again!");
});

promise
.then(res => console.log(res))
.catch(err => console.log(err));

// => 'Yay!'
  • If a promise has succeeded or failed and you later add a success/failure callback, the correct callback will be called, even though the event took place earlier. This is extremely useful for async success/failure, because you’re less interested in the exact time something became available, and more interested in reacting to the outcome.

Purpose of Promises — Replacing Callbacks

  • Nesting callbacks can often get messy and troublesome. With promises, all you have to do is add another .then block. The result is code that is much cleaner to write.
const promise = new Promise(resolve => resolve(1));
promise
.then((num) => {
return num + 1; // num is now 2
})
.then((num) => {
return num + 2; // num is now 4
})
.then((num) => {
return num + 3; // num is now 7
})
.finally(() => {
console.log('Done!');
});
  • In the code above, there are 3 then callbacks which are invoked in the order shown. Each one passes its return value to the next callback in the chain.
  • When a Promise is settled (either resolved or rejected), finally is invoked. Like then and catch, finally returns an equivalent Promise object that can be chained to another promise method. This lets you avoid duplicating code in the then and catch handlers.
  • Note that the callback for finally takes no arguments (passing num would have have set num to undefined regardless of what happened in the then clauses).

Running promises in parallel

  • JavaScript doesn’t wait for a Promise to finish executing before starting the next one. This allows us to run promises in parallel.
  • We can then use what’s called Promise.all to pass in an iterable array of all the different promises that we want to run, which then returns a single Promise that resolves to an array of the results of the input promises.
const connectToServer1 = new Promise ((resolve, reject) => {
resolve('Connected to server 1');
});

const connectToServer2 = new Promise ((resolve, reject) => {
resolve('Connected to server 2');
});

const connectToServer3 = new Promise ((resolve, reject) => {
resolve('Connected to server 3');
});

Promise.all([
connectToServer1,
connectToServer2,
connectToServer3
]).then((messages) => {
console.log(messages);
})

// [ 'Connected to server 1', 'Connected to server 2', 'Connected to server 3' ]
  • All promises in Promise.all run at the exact same time. For example, connectToServer1 could’ve taken a long time if the server was experiencing a slowdown, while the other 2 could’ve been very quick. The other two won't have to wait for the first one to finish.
  • Promise.all is going to run every single one of these promises and when it's done, it's going to call the .then or .catch method depending on if they resolved or failed.
  • If all of the promises inside the array resolve, then an array containing all of the successful messages gets output. However, if one or more promises get rejected then the first one that rejects is caught in the .catch block and its failure message is output.
  • For example, let’s say you were using promises to load 3 images. If 2 out of 3 successfully resolve and 1 rejects, then Promise.all will immediately reject, causing every other image not to load.

Promise.allSettled()

  • The Promise.allSettled method returns a single Promise object that resolves after all of the given promises have either fulfilled/resolved or rejected. The returned Promise object contains an object with two properties for each provided promise: status and value or reason.
  1. status — can be “fulfilled” or “rejected”
  2. value— contains the resolved value of a fulfilled promise
    reason — contains the reason of a rejected promise
const promise1 = Promise.resolve('foo');
const promise2 = new Promise((_, reject) => setTimeout(reject, 100, 'bar'));
const promises = [promise1, promise2];

Promise.allSettled(promises).then(results => {
results.forEach(result => console.log(result));
});
{ status: 'fulfilled', value: 'foo' }
{
status: 'rejected', reason: 'bar' }

Do something as soon as one asynchronous operation finishes

  • Let’s say you wanted to do something as soon as one Promise had settled, instead of waiting for all of them to settle before accomplishing some task.
  • For this purpose, use Promise.race instead of Promise.all.
Promise.race([
loadImageOne,
loadImageTwo,
loadImageThree
]).then(message => {
console.log(message);
})
  • They’re both almost the same, except that Promise.race will return as soon as one Promise is settled. Because of that, it will return a single message as opposed to all of the messages.

Promise.any()

  • Promise.any fulfills with the first promise to fulfill (i.e., resolve) even if another promise rejects first. If all of them get rejected, it returns an AggregateError message.
const promise1 = new Promise((resolve, _) => { 
setTimeout(resolve, 500, 'foo');
});

const promise2 = new Promise((_, reject) => {
setTimeout(reject, 100, 'bar');
});

const promises = [promise1, promise2];
Promise.any(promises)
.then(result => console.log(result)) // => foo
.catch(error => console.log(error));
  • In contrast, Promise.race will fulfill as soon as one promise settles (either resolved or rejected).
Promise.race(promises)
.then(result => console.log(result))
.catch(error => console.log(error)); // => bar
  • If all promises were to be rejected, Promise.any logs an AggregateError message that includes the error messages for each:
[AggregateError: All promises were rejected] {
[errors]: [ 'foo', 'bar' ]
}
  • Promise.any focuses on retrieving data as fast as possible. By waiting for the first successful response and ignoring failed ones, Promise.any allows us to create more reliable code. For example, making multiple network requests in case one of them fails. Those savings can be passed onto the user by improving response times.
  • For example, let’s say you’re trying to retrieve data from some API endpoint and the request fails. You retry a few more times but your reconnect attempts don’t go through. At this point, you can use Promise.any and make multiple requests to servers located further away and see if at least one of them is able to provide the data you’re seeking. This would allow you to recover quicker, while trying to figure out what’s causing the slowdown on the main server.

Creating a time limit for an async task

  • By racing an async task such as an API call against a promise that is going to be rejected after a set period of time, we effectively create a time limit for the async task.

reject/resolve using Promise.race:

const timeout = new Promise((_, reject) => {
setTimeout(reject, 500, 'error: time limit exceeded');
});


const promise = new Promise((resolve, _) => {
fetch("https://www.boredapi.com/api/activity")
.then(response => response.json())
.then(data => resolve(data.activity));
})

Promise.race([timeout, promise])
.then(msg => console.log(msg))
.catch(msg => console.log(msg))

resolve/resolve using Promise.any:

const timeout = new Promise((resolve, _) => {
setTimeout(resolve, 500, 'too slow');
});


const promise = new Promise((resolve, _) => {
fetch("https://www.boredapi.com/api/activity")
.then(response => response.json())
.then(data => resolve(data.activity));
})

Promise.any([timeout, promise]).then(msg => console.log(msg))

Summary

  • Promise.all is useful anytime you have more than one promise and your code wants to know when all the operations that those promises represent have finished successfully.
  • Promise.race looks for the first settled (rejected or resolved) promise.
  • Promise.any looks for the first fulfilled promise.

--

--