Glad Chinda Full-stack web developer learning new hacks one day at a time. Web technology enthusiast. Hacking stuffs @theflutterwave.

Improve async programming with JavaScript promises

17 min read 4815

Improve async programming with JavaScript promises

Editor’s note: This post was edited and updated in August 2021 to include new information.

If you’ve written asynchronous JavaScript code before, then you already have an idea about using callbacks and the issues with them. One major issue with using callbacks is the possibility of running into callback hell.

In ES2015, JavaScript promises were added to the language spec, bringing about an entirely new shift in how asynchronous code is written, and also mitigating the issue of running into callback hell. If you are using ES2015 syntax in your code, you may already be familiar with promises.

In this guide, you’ll learn some practical ways to improve asynchronous programming in JavaScript using promises, including:

Note: This guide is not in any way an introduction to JavaScript promises. Some prior knowledge of promises is required to read this guide.

Creating promises

A JavaScript promise can be created using the Promise constructor. The constructor takes an executor function as its argument, which is immediately executed to create the promise.

The executor, in turn, can take two callback functions as its arguments that can be invoked within the executor function in order to settle the promise, namely:

  • resolve for fulfilling the promise with a value
  • reject for rejecting the promise with a reason (usually an error)

Here is a very simple JavaScript promise:

const isLessThan10 = (num) => {
  new Promise((resolve, reject) => {
    if (num < 10) {
      resolve("Correct");
    } else {
      reject("Wrong!!");
    }
  })
    .then((res) => console.log(res))
    .catch((err) => console.log(err));
};

isLessThan10(14); 

If you run the above code, you will see “Wrong!!” in your console, meaning the promise got rejected. That is because 14 is obviously not less than 10, but when you pass a number lesser than 10, the promise will be fulfilled.

Promise states

In the section above, you must have noticed our use of two words: rejected and fulfilled. These are two of the three states of a JavaScript promise. Let’s talk about the three possible states of a promise.

We made a custom demo for .
No really. Click here to check it out.

  • rejected – a promise is rejected when the operation fails, e.g., above in the isLessThan10 function, when we passed 14, the promise was rejected
  • fulfilled – a promise is fulfilled when the operation works or is correct, e.g., in the function above, passing a number less than 10 fulfills the promise
  • Pending – a promise is pending when it is waiting to be resolved or rejected. A promise only gets to this state when then operation is asynchronous

A promise is only fulfilled when it is resolved using a promise resolve argument. promise.resolve fulfills a promise with a value, while a promise is rejected with the promise reject argument. These two states show the promise was settled and is no longer pending.

Settled promises

Often times, you just want to create a promise that is already settled — either fulfilled with a value, or rejected with a reason. For cases like this, the Promise.resolve() and Promise.reject() methods come in handy. Here is a simple example:

// This promise is already fulfilled with a number (100)
const fulfilledPromise = Promise.resolve(100);

// This promise is already rejected with an error
const rejectedPromise = Promise.reject(new Error('Operation failed.'));

// Getting the rsolved value of the promise
fulfilledPromise.then(res => console.log(res));

// Getting catching to see the error of the promise
rejectedPromise.then(res => console.log(res)).catch(err => console.log(err.message));

There could also be times when you are not sure if a value is a promise or not. In cases like this, you can use Promise.resolve() to create a fulfilled promise with the value and then work with the returned promise. Here is an example:

// User object
const USER = {
  name: 'Glad Chinda',
  country: 'Nigeria',
  job: 'Fullstack Engineer'
};

// Create a fulfilled promise using Promise.resolve()
Promise.resolve(USER)
  .then(user => console.log(user.name));

Handling promises

A settled promise can be handled by passing callbacks to the then(), catch(), or finally() methods of the promise, as seen above in some earlier code snippets. Here, we will refactor the isLessThan10 function and see how to handle rejected and fulfilled promises.

const isLessThan10 = (num) => {
  return new Promise((resolve, reject) => {
    if (num < 10) {
      resolve("Correct");
    } else {
      reject("Wrong!!!");
    }
  })
};

// Case1
isLessThan10(1)
  .then(console.log)
  .catch(console.error);

// Case2
// Alternatively, the rejection can be handled in the same .then() call
// By passing the rejection handler as second argument to .then()
isLessThan10(12).then(console.log, console.error);

Apart from using .catch() to handle rejected promises, as seen above, we can also pass two callbacks to .then(). The first will handle the promise if it is fulfilled, while the other will handle it if rejected. We can also manipulate the resolved value of the promise in the then() block.

.finally() is always executed once the promise is settled, regardless of whether it is fulfilled or rejected. It is a good place to perform cleanup actions like resetting a variable or clearing a state.

const isLessThan10 = (num) => {
  return new Promise((resolve, reject) => {
    if (num < 10) {
      resolve("Correct");
    } else {
      reject("Wrong!!!");
    }
  })
    .then(111)
    .catch(222);
};

isLessThan10(11)
  .then((res) => console.log(res))
  .catch((err) => console.error(err))
  .finally(() => console.log("This promise is finally settled!"));

Dumb then handlers

The .then() method can take up to two handler functions as its arguments: fulfillment handler and rejection handler.

However, if any of these two arguments are not a function, .then() replaces that argument with a function and continues with the normal execution flow. It becomes important to know what kind of function the argument is replaced with. Here is what it is:

  • If the fulfillment handler argument is not a function, it is replaced with an Identity Function. An identity function is a function that simply returns the argument it receives
  • If the rejection handler argument is not a function, it is replaced with a Thrower Function. A thrower function is a function that simply throws the error or value it receives as its argumentThe identity and thrower functions written out

Here is a simple example:

const isLessThan10 = (num) => {
  return new Promise((resolve, reject) => {
    if (num < 10) {
      resolve("Correct");
    } else {
      reject("Wrong!!!");
    }
  })
    .then(111) // Just a random number 
    .catch(222); // Just a random number
};

//This will log 'Correct' to the console
isLessThan10(3).then(res => console.log(res)).catch(err => console.error(err));

// This will log 'Wrong' to the console
isLessThan10(13).then(res => console.log(res)).catch(err => console.error(err));

If you observe carefully, you will notice that neither the identity function nor the thrower function alters the normal execution flow of the promise sequence. They simply have the same effect as omitting that particular .then() call in the promise chain. For this reason, I usually refer to these handler arguments as “dumb handlers”.

.then() handlers always return promises

One important thing to understand about the .then() promise method is that it always returns a promise.

Here is a breakdown of how .then() returns a promise based on what is returned from the handler function passed to it:

Timing with promises

Delaying execution

Promises can be very useful for timing applications. Some programming languages like PHP have a sleep() function that can be used to delay the execution of an operation until after the sleep time.

While a sleep() function does not exist as part of the JavaScript spec, the global setTimeout() and setInterval() functions are commonly used for executing time-based operations.

The setInterval() method is a JavaScript function used to execute a code block at specified time with delays between each call, while the setTimeout() method is used to add a timer to a JavaScript code block.

Here is how the sleep() function can be simulated using promises in JavaScript. However, in this version of the sleep() function, the halt time will be in milliseconds instead of seconds:

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

Here is a slightly expanded and annotated version of the sleep(ms) function:

const sleep = ms => {
  // Return a new promise
  // No need defining the executor function with a `reject` callback
  return new Promise(resolve => {
    // Pass resolve as the callback to setTimeout
    // This will execute `resolve()` after `ms` milliseconds
    setTimeout(resolve, ms);
  });
}

The sleep(ms) function can can even be improved further to be come a self-contained delay function that executes a callback function after the specified sleep time.

Here is what using the sleep() function could look like:

// Sleep for 5 seconds
// Then execute the operation
sleep(5000).then(executeOperation);

// Delay function
// Using async/await with sleep()
const delay = async (callback, seconds = 1) => {
  // Sleep for the specified seconds
  // Then execute the operation
  await sleep(seconds * 1000);
  callback();
}

// Using the `delay()` function
// Execution delayed by 5 seconds
delay(executeOperation, 5);

Measuring execution time

What if you are interested in knowing how long it took for an asynchronous operation to be completed? This is usually the case when benchmarking the performance of some form of implementation or functionality.

Here is a simple implementation that leverages a JavaScript promise to compute the execution time for an asynchronous operation.

const timing = callback => {
  // Get the start time using performance.now()
  const start = performance.now();

  // Perform the asynchronous operation
  // Finally, log the time difference
  return Promise.resolve(callback())
    .finally(() => console.log(`Timing: ${performance.now() - start}`));
}

In this implementation, performance.now() is used instead of Date.now() for getting the timestamp with a higher resolution. For non-browser environments where the performance object does not exist, you can fallback on using Date.now() or other host implementations.

In the code block below, the timing() function could be used to log the execution time of an asynchronous operation on the console:

// Async operation that takes between 1 - 5 seconds
const asyncOperation = () => new Promise(resolve => {
  setTimeout(() => resolve('DONE'), Math.ceil(Math.random() * 5) * 1000);
});

// Compute execution time in ms
// And log it to the console
timing(asyncOperation); // Timing: 4003.4000000014203

Sequential execution with promises

With JavaScript promises, you can execute asynchronous operations in sequence. This is usually the case when a later asynchronous operation depends on the execution of a former asynchronous operation, or when the result of a former asynchronous operation is required for a later operation.

Executing asynchronous operations in sequence usually involves chaining one or more .``then() and .catch() handlers to a promise. When a promise is rejected in the chain, it is handled by the rejection handler defined in the next .then() handler in the chain and then execution continues down the chain.

However, if no rejection handler has been defined in the next .then() handler in the chain, the promise rejection is cascaded down the chain until it reaches the first .catch() handler.

Case study: Photo gallery application

Let’s say you are building a photo gallery application, and you want to be able to fetch photos from an online photo repository, then filter them by format, aspect-ratio, dimension ranges, etc.

Here are some possible functions you could have in your application:

/**
 * Fetches photos from the Picsum API
 * @returns {Promise} A promise that is fulfilled with an array of photos from the Picsum repository
 */

const fetchPhotos = () =>
  fetch('https://picsum.photos/list')
    .then(response => response.json());

/**
 * Filters photos and returns only JPEG photos 
 * @param {Array} photos
 * @returns {Array} An array of JPEG photos
 */
const jpegOnly = photos =>
  photos.filter(({ format }) => format.toLowerCase() === 'jpeg')

/**
 * Filters photos and returns only square photos
 * @param {Array} photos
 * @returns {Array} An array of square photos
 */

const squareOnly = photos =>
  photos.filter(({ width, height }) => height && Number.isFinite(height) && (width / height) === 1)

/**
 * Returns a function for filtering photos by size based on `px`
 * @param {number} px The maximum allowed photo dimension in pixels
 * @returns {Function} Function that filters photos and returns an array of photos smaller than `px`
 */

const smallerThan = px => photos =>
  photos.filter(({ width, height }) => Math.max(width, height) < px)

/**
 * Return an object containing the photos count and URLs.
 * @param {Array} photos
 * @returns {Object} An object containing the photos count and URLs
 */

const listPhotos = photos => ({
  count: photos.length,
  photos: photos.map(({ post_url }) => post_url)
})

In the code block above, the fetchPhotos() function fetches a collection of photos from the Picsum Photos API using the global fetch() function provided by the Fetch API, and returns a promise that is fulfilled with a collection of photos.

Here is what the collection returned from the Picsum Photos API looks like:

The collection returned by the Picsum Photos API

The filter functions accept a collection of photos as an argument and filter the collection in some manner of the following ways:

  • jpegOnly()  —  filters a photo collection and returns a sub-collection of only JPEG images
  • squareOnly()  —  filters a photo collection and returns a sub-collection of only photos with a square aspect-ratio
  • smallerThan()  — this is a higher-order function that takes a dimension and returns a photos filter function that returns a sub-collection of photos whose maximum dimensions are smaller than the specified dimension threshold

Let’s say we want to execute this sequence of operations:

  1. Fetch the photos collection
  2. Filter the collection leaving only JPEG photos
  3. Filter the collection leaving only photos with a square aspect-ratio
  4. Filter the collection leaving only photos smaller than 2500px
  5. Extract the photos count and URLs from the collection
  6. Log the final output on the console
  7. Log error to the console if an error occurred at any point in the sequence

The following code snippet shows how we can chain the execution of these operations in a promise sequence:

// Execute asynchronous operations in sequence
fetchPhotos()
  .then(jpegOnly)
  .then(squareOnly)
  .then(smallerThan(2500))
  .then(listPhotos)
  .then(console.log)
  .catch(console.error);

The above code will output a result similar to the image below:

The output of our asynchronous operations sequence

Running and executing JavaScript promises in parallel

With JavaScript promises, you can execute multiple independent, asynchronous operations in batches or in parallel using the Promise.all() method.

Promise.all() accepts an iterable of promises as its argument and returns a promise that is fulfilled when all the promises in the iterable are fulfilled, or is rejected when one of the promises in the iterable is rejected.

If the returned promise fulfills, it is fulfilled with an array of all the values from the fulfilled promises in the iterable (in the same order). However, if it rejects, it is rejected because of the first promise in the iterable that rejected.

Case study: Current temperatures

Let’s say you are building a weather application that allows users to see the current temperatures of a list of cities they’ve selected.

Using Promise.all(), you can make a GET request to the weather API to fetch the temperature of all the selected cities at once, so your users won’t see the data rendering one after the other on your app.

The following code snippet demonstrates how to fetch the current temperatures of the selected cities in parallel with Promise.all().

The OpenWeatherMap API service will be used to fetch the weather data, so if you’d like to follow along, head over to their website following that link and sign up to get an API key.

// Use your OpenWeatherMap API KEY
// Set the current weather data API URL
const API_KEY = 'YOUR_API_KEY_HERE';
const API_URL = `https://api.openweathermap.org/data/2.5/weather?appid=${API_KEY}&units=metric`;

// Set the list of cities
const CITIES = [
  'London', 'Tokyo', 'Melbourne', 'Vancouver',
  'Lagos', 'Berlin', 'Paris', 'Johannesburg',
  'Chicago', 'Mumbai', 'Cairo', 'Beijing'
];

const fetchTempForCity = city => {
  return fetch(`${API_URL}&q=${encodeURIComponent(city)}`)
    .then(response => response.json())
    .then(data => [ city, data.main.temp || null ]);
}

const fetchTempForCities = cities => {
  return Promise.all(cities.map(fetchTempForCity))
    .then(temps => {
      return temps.reduce((data, [ city, temp ]) => {
        return { ...data, [city]: Number.isFinite(temp) ? temp.toFixed(2) * 1 : null };
      }, {});
    });
}

fetchTempForCities(CITIES)
  .then(console.log, console.error);

In the code block above, we created the functions below:

  • fetchTempForCity() — accepts a single city as its argument and returns a promise that is fulfilled with the current temperature of the given city (in °C) by calling the OpenWeatherMap API service. The returned promise is fulfilled with an array of the format: [city, temperature]
  • fetchTempForCities() — accepts an array of cities and fetches the current temperature of each city by leveraging Array.prototype.map() to call the fetchTempForCity() function on each city.

The Promise.all() method is used to run the requests in parallel and accumulate their data into a single array, which, in turn, is reduced to an object using an Array.prototype.reduce() function .

The code snippet above will return an object similar to the result below:

The OpenWeather API code snippet results

Rejection handling

It is important to note that  if any of the fetch temperature promises passed into Promise.all() are rejected with a reason, the entire promise batch will be rejected immediately with that same reason.

That is, if at least one out of the twelve fetch temperature promises is rejected for some reason, the entire promise batch will be rejected, and hence, no temperature will be returned from the promise.

The scenario described above is usually not the desired behavior  in most cases — a failed temperature fetch should not cause the results of the successful fetches in the batch to be discarded. We can easily fix this by using another promise method promise.allSettled(), which will be talking about below, but there’s also another simple workaround.

The simple workaround for this is to attach a .catch() handler to the fetchTempForCity promise, causing it to fulfill the promise with a null temperature value in cases of rejection.

This is what it will look like:

const fetchTempForCity = city => {
  return fetch(`${API_URL}&q=${encodeURIComponent(city)}`)
    .then(response => response.json())
    .then(data => [ city, data.main.temp || null ])

    // Attach a `.catch()` handler for graceful rejection handling
    .catch(() => [ city, null ]);
}

With that little change to the fetchTempForCity() function, there is now a very high guarantee that the returned promise will never be rejected in cases where the request fails or something goes wrong. Rather, it will be fulfilled with an array of the format: [city, null], like the below:

The city, null formatting
With this change, it becomes possible to further improve the code to be able to schedule retries for failed temperature fetches.

The following code snippet includes some additions that can be made to the previous code to make this possible.

// An object that will contain the current temperatures of the cities

// The keys are the city names, while the values are their current temperatures (in °C)

let TEMPS = null;

// The maximum number of retries for failed temperature fetches

const MAX_TEMP_FETCH_RETRIES = 5;

// Fetches the current temperatures of multiple cities (in °C) and update the `TEMPS` object.

const fetchTemperatures = (cities, retries = 0) => {
  return fetchTempForCities(cities)
    .then(temps => {

      // Update the `TEMPS` object with updated city temperatures from `temps`
      TEMPS = (TEMPS === null) ? temps : { ...TEMPS, ...temps };

      // Filter the keys (cities) of the `TEMPS` object to get a list of the cities
      // with `null` temperature values.

      const RETRY_CITIES = Object.keys(TEMPS)
        .filter(city => TEMPS[city] == null);

      // If there are 1 or more cities in the `RETRY_CITIES` list
      // and the maximum retries has not been exceeded,
      // attempt to fetch their temperatures again after waiting for 5 seconds.
      // Also increment `retries` by 1.

      if (RETRY_CITIES.length > 0 && retries < MAX_TEMP_FETCH_RETRIES) {
        setTimeout(() => fetchTemperatures(RETRY_CITIES, ++retries), 5 * 1000);
      }

      // Return the updated `TEMPS` object
      return TEMPS;

    })
    .then(console.log, console.error);
}
// Fetch the current temperatures of the cities in the `CITIES` list
// and update the `TEMPS` object

fetchTemperatures(CITIES);

In this code snippet, the TEMPS object is used to hold the updated temperatures of the listed cities. The MAX_TEMP_FETCH_RETRIES constant is an integer that limits the number of retries that can be done for failed fetches, which is five (5) in this case.

The fetchTemperatures() function receives an array of city names and the number of retries so far as its arguments. It calls fetchTempForCities() to fetch the current temperatures for the cities passed to it, and also updates the TEMPS object with the temperatures.

For failed fetches, the function schedules another call to itself after waiting for five seconds and increments the retries count by 1. The retries are done for as many times as possible, provided the set maximum has not be exceeded  —  which is five, in our case.

Waiting for all to be Settled

Just as promise.all() and promise.race() handle multiple promises, there is another very useful one, promise.allSettled(), which was added to the JavaScript specification with ES2020.

It is very similar to promise.all(), but unlike it, promise.allSettled() isn’t rejected when any of the promises in the iterable passed to it are rejected. Instead, it waits for all promises to be settled (fulfilled or rejected) and then returns an array containing the result of each promise. Let’s see an example below.

const promise1 = Promise.resolve("I got fulfilled!");
const promise2 = Promise.reject("I was rejected!");
Promise.allSettled([promise1, promise2]).then((results) =>
  console.log(results)
);

The above code will return the a result like the one below:

[
  { status: 'fulfilled', value: 'I got fulfilled!' },
  { status: 'rejected', reason: 'I was rejected!' }
]

Now, let’s refactor the OpenWeatherAPI code snippet we wrote above when we discussed promise.all(), and we implemented a possible workaround for cases where one of the promise is rejected by catching the errors.

With promise.allSettled(), we don’t need that workaround. It will work just fine and we will also see the rejected promise coupled with the reason. Let’s refactor the code below:

// Use your OpenWeatherMap API KEY
// Set the current weather data API URL

const API_KEY = "YOUR_API_KEY_HERE";
const API_URL = `https://api.openweathermap.org/data/2.5/weather?appid=${API_KEY}&units=metric`;

// Set the list of cities

const CITIES = [
  "Lagos",
  "Berlin",
  "Parwis",  // Tweaked this to cause an error
];

const fetchTempForCity = (city) => {
  return fetch(`${API_URL}&q=${encodeURIComponent(city)}`)
    .then((response) => response.json())
    .then((data) => [city, data.main.temp]);
};

const fetchTempForCities = (cities) => {
  return Promise.allSettled(cities.map(fetchTempForCity)).then((temps) => temps);
};

fetchTempForCities(CITIES).then(console.log, console.error);

The result should be like this:

[
  { status: "fulfilled", value: ["Lagos", "24.18"]},
  { status: "fulfilled", value: ["Berlin", "13.83"]},
  { status: "rejected", 
    reason: TypeError: Cannot read properties of undefined(reading 'temp')
  },
]

Note: If an empty array is passed to promise.settled(), it will return a resolved promise with an empty array as the value.

Racing async operations with promises

With JavaScript promises, you can race multiple independent asynchronous operations using the Promise.race() method. Promise.race() accepts an iterable of promises as its argument and returns a promise that is fulfilled or rejected in the same way as the first settled promise in the iterable.

If the first settled promise in the iterable is fulfilled with a value, the race promise is fulfilled with the same value. However, if it is rejected, the race promise will be rejected with the same reason. If multiple promises are fulfilled or rejected at the same time, then the first promise will be used based on the order of the promises in the iterable.

If the iterable passed to Promise.race() is empty, then the race promise remains pending forever and is never settled.

Case study: Timeout response

Let’s say you are building an API endpoint that does some asynchronous operation, like reading from a file or querying a database, and you want to guarantee that you get a response in 5 seconds , otherwise the request should fail with a HTTP status code of 504 (the Gateway Timeout response).

The following code snippet demonstrates how Promise.race() can be used to achieve this, assuming we are building the API using the Express.js framework for Node.js.

// Create a new Express app and set the port
const app = require('express')();
const PORT = process.env.PORT || 5000;

// The timeout in seconds for API responses
const TIMEOUT_SECONDS = 5;

// Define a new route on the Express app: GET /random
app.get('/random', (req, res) => {

  /**
   * `execute` is a promise that simulates a time-consuming asynchronous operation
   * which may take anywhere between 1s - 10s to complete its execution.
   * On completion, it is fulfilled with an object that looks like this:
   * {
   *   statusCode: 200,
   *   random: (A random integer in the range of 0 - 100, both inclusive)
   *   duration: (The duration of the execution in seconds, expressed as {duration}s)
   * }
   */

  const execute = new Promise(resolve => {
    // Random execution time in milliseconds
    const timeInMs = Math.floor((Math.random() * 10) * 1000);

    // Simulate execution delay using setTimeout and fulfill the promise
    // with the response object
    setTimeout(() => {
      resolve({
        statusCode: 200,
        random: Math.floor(Math.random() * 101),
        duration: `${timeInMs / 1000}s`
      })
    }, timeInMs);
  });

  /**
   * `requestTimer` is a promise that is settled after `TIMEOUT_SECONDS` seconds
   * On completion, it is fulfilled with an object that looks like this:
   * { statusCode: 504 }
   * which represents a Gateway Timeout on the server.
   */

  const requestTimer = new Promise(resolve => {
    // Simulate execution delay using setTimeout and fulfill the promise
    // with the response object
    const timeoutInMs = TIMEOUT_SECONDS * 1000;
    setTimeout(() => resolve({ statusCode: 504 }), timeoutInMs);
  });

  /**
   * `Promise.race()` is used to run both the `execute` and the `requestTimer` promises.
   * The first of the two promises that gets settled will be used to settle the race promise.
   * The fulfilled response object is then used to form and send the HTTP response.
   * If an error occurs, a HTTP 500 error response is sent.
   */
  return Promise.race([ execute, requestTimer ])
    .then(({ statusCode = 200, ...data }) => {
      const response = res.status(statusCode);

      return (statusCode == 200)
        ? response.json(data)
        : response.end();
    })
    .catch(() => res.status(500).end());

});

// Start the app on the set port

app.listen(PORT, () => console.log(`App is running on port ${PORT}.`));

In this code snippet, a very minimalistic Express application has been set up with a single route  — GET/random for returning a randomly generated integer in the range of 0–100 (both inclusive), while also returning the execution time.

Promise.race() is used to wait for the first of two promises:

  • an execute promise that performs some seemingly time-consuming asynchronous operation and gets settled after 1s — 10s
  • a requestTimer promise that does nothing and gets settled after the set TIMEOUT_SECONDS seconds, which is 5 seconds in this case

So, here is what happens: whichever of these two promises that is settled first will determine the final response from the endpoint  —  Promise.race() will make sure of that.

A similar technique can also be used when handling fetch events in service workers to detect slow networks.

Waiting for the first fulfilled promise

In situations where we want to return the first fulfilled promise, promise.any() comes in handy. Unlike promise.race() that returns the first fulfilled or rejected promise, promise.any() returns the first fulfilled promise. If no promise is fulfilled, it will return a rejected promise with an AggregateError object.

Of the four promise methods, (any(), race(), allSettled(), and all(), only promise.allSettled() executes all the promises passed to it because it waits for all promises to be resolved. The others do not, and so they are said to have a short circuit.

Below is a table from a GitHub gist created by Sung M. Kim showing the differences between promise.all(), promise.allSettled(), promise.race(), and promise.any().

Using async/await with promises

Async and await are keywords that can make writing promises cleaner. Preceding a function with the keyword async will make the function return a promise, which then permits it an await keyword inside. With await, we can wait for the promise to be resolved. With async and await, we don’t need to use .then(). Let’s see an example below using the OpenWeather API example once more:

// Set the current weather data API URL
const API_KEY = "YOUR_API_KEY";
const API_URL = `https://api.openweathermap.org/data/2.5/weather?appid=${API_KEY}&units=metric`;

const fetchTempForCity = async (city) => {
  let response = await fetch(`${API_URL}&q=${encodeURIComponent(city)}`);
  response = await response.json();
  console.log(response)
};

fetchTempForCity('Port harcourt');

We can handle possible errors with try…catch method. If the promise is rejected, we will get the error in the catch block.

const fetchTempForCity = async (city) => {
  try {
    let response = await fetch(`${API_URL}&q=${encodeURIComponent(city)}`);
    response = await response.json();
    console.log(response);
  } catch (error) {
    console.error(error.message)
  }
};

fetchTempForCity('Port harcourt');

Conclusion

JavaScript promises can drastically change the way you go about writing asynchronous programs, making your code more succinct and clearer in regard to the desired intent.

In this guide, we looked at several ways promises can be used in asynchronous programs, like:

  • Executing operations in sequence, in parallel, and even racing them
  • How to execute multiple promises and wait for all to be resolved
  • How to execute promises to be terminated as soon as one is fulfilled

We also saw how to use async/await functions and await keyword handle promises. You can learn more about promises from MDN’s JavaScript promise docs.

: Debug JavaScript errors easier by understanding the context

Debugging code is always a tedious task. But the more you understand your errors the easier it is to fix them.

LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to find out exactly what the user did that led to an error.

LogRocket records console logs, page load times, stacktraces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!

.
Glad Chinda Full-stack web developer learning new hacks one day at a time. Web technology enthusiast. Hacking stuffs @theflutterwave.

Testing accessibility with Storybook

One big challenge when building a component library is prioritizing accessibility. Accessibility is usually seen as one of those “nice-to-have” features, and unfortunately, we’re...
Laura Carballo
4 min read

Leave a Reply