Monday, March 27, 2023

Using the Promise.all() and Promise.allSettled() Methods in JavaScript

Using the Promise.all() and Promise.allSettled() Methods in JavaScript

This tutorial will teach you how to use promises to wait in JavaScript.

In this tutorial, I will teach you about the Promise.all() and Promise.allSettled() methods and how you can use them to work with multiple promises.

Using the Promise.all() Method

The Promise object has three useful methods named then(), catch(), and finally() that you can use to execute callback methods when the promise has settled.

The Promise.all() method is a static method, which means that it belongs to the whole class instead of being tied to any specific instance of the class. It accepts an iterable of promises as input and returns a single Promise object.

As I mentioned earlier, the Promise.all() method returns a new Promise. This new promise will resolve to an array of values of settled promises if all the promises passed to the method have resolved successfully. This new promise will also be settled with a rejection as soon as one of the passed promises gets rejected.

All Promises Resolve Successfully

Here is an example of the Promise.all() method where all the promises resolved successfully:

1
const promise_a = new Promise((resolve) => {
2
  setTimeout(() => {
3
    resolve('Loaded Textures');
4
  }, 3000);
5
});
6
7
const promise_b = new Promise((resolve) => {
8
    setTimeout(() => {
9
      resolve('Loaded Music');
10
    }, 2000);
11
});
12
13
const promise_c = new Promise((resolve) => {
14
    setTimeout(() => {
15
      resolve('Loaded Dialogues');
16
    }, 4000);
17
});
18
19
20
const promises = [
21
  promise_a, promise_b, promise_c
22
];
23
24
console.log('Hello, Promises!');
25
26
Promise.all(promises).then((values) => {
27
  console.log(values);
28
  console.log('Start the Game!');
29
});
30
31
/* Output

32


33
19:32:06 Hello, Promises!

34
19:32:10 Array(3) [ "Loaded Textures", "Loaded Music", "Loaded Dialogues" ]

35
19:32:10 Start the Game!

36


37
*/

Our statement before the call to the Promise.all() method was logged at 19:32:06. Also, our third promise named promise_c takes the longest to settle and resolves after 4 seconds. This means that the promise returned by the call to the all() method should also take 4 seconds to resolve. We can verify that it does take 4 seconds to resolve by passing a callback function to the then() method.

Another important thing to note here is that the returned array of fulfilled values contains those values in the same order in which we passed the promises to the Promise.all() method. The promise named promise_b resolves the quickest, in 2 seconds. However, its resolved value is still in the second position in the returned array. This matches the position at which we passed the promise to the Promise.all() method.

This maintenance of order can be very helpful in certain situations. For example, let's say you're fetching information about the weather in ten different cities using ten different promises. All of them are not going to resolve at the same time, and the order in which they will be resolved isn't likely to be known beforehand. However, if you know that the data is returned in the same order in which you passed the promise, you will be able to assign it properly for later manipulation.

One Promise Rejected

Here is an example where one of the promises is rejected:

1
const promise_a = new Promise((resolve) => {
2
  setTimeout(() => {
3
    resolve('Loaded Textures');
4
  }, 3000);
5
});
6
7
const promise_b = new Promise((resolve, reject) => {
8
    setTimeout(() => {
9
      reject(new Error('Could Not Load Music'));
10
    }, 2000);
11
});
12
13
const promise_c = new Promise((resolve) => {
14
    setTimeout(() => {
15
      resolve('Loaded Dialogues');
16
    }, 4000);
17
});
18
19
20
const promises = [
21
  promise_a, promise_b, promise_c
22
];
23
24
console.log('Hello, Promises!');
25
26
Promise.all(promises).catch((error) => {
27
  console.error(error.message);
28
  console.log('Stop the Game!');
29
});
30
31
/* Output

32


33
20:03:43 Hello, Promises!

34
20:03:45 Could Not Load Music

35
20:03:45 Stop the Game!

36


37
*/

Again, our statement before the call to the all() method was logged at 20:03:43. However, our second promise promise_b settled with a rejection this time. We can see that promise_b was rejected after 2 seconds. This means that the promise returned by the all() method should also reject after 2 seconds with the same error as our promise_b. It is evident from the output that this is exactly what happened.

Usage With the await Keyword

You probably already know that the await keyword is used to wait for a promise to resolve before proceeding further. We also know that the all() method returns a single promise. This means that we can use await along with a call to the Promise.all() method.

The only thing to keep in mind is that since await is only valid inside async functions and modules, we will have to wrap our code inside an async function, as shown below:

1
function create_promise(data, duration) {
2
  return new Promise((resolve) => {
3
    setTimeout(() => {
4
      resolve(data);
5
    }, duration);
6
  });
7
}
8
9
const promise_a = create_promise("Loaded Textures", 3000);
10
const promise_b = create_promise("Loaded Music", 2000);
11
const promise_c = create_promise("Loaded Dialogue", 4000);
12
13
const my_promises = [promise_a, promise_b, promise_c];
14
15
async function result_from_promises(promises) {
16
  let loading_status = await Promise.all(promises);
17
  console.log(loading_status);
18
}
19
20
result_from_promises(my_promises);
21
22
/* Outputs

23


24
08:50:43 Hello, Promises!

25
08:50:47 Array(3) [ "Loaded Textures", "Loaded Music", "Loaded Dialogue" ]

26


27
*/

This time, we have defined a function called create_promise() that creates promises for us based on the provided data and duration. Our async result_from_promises() function uses the await keyword to wait for the promises to resolve.

Using the Promise.allSettled() Method

It makes sense to use the Promise.all() method when you only want to proceed after all the promises resolve successfully. This could be useful when you are loading resources for a game, for example.

However, let's say you are getting information about the weather in different cities. In this case, it would make sense for you to output the weather information for any cities where fetching the data was successful and output an error message where fetching the data failed.

The Promise.allSettled() method works best in this case. This method waits for all the passed promises to settle either with a resolution or with a rejection. The promise returned by this method contains an array of objects which contain information about the outcome of each promise.

1
function create_promise(city) {
2
  let random_number = Math.random();
3
  
4
  let duration = Math.floor(Math.random()*5)*1000;
5
6
  return new Promise((resolve, reject) => {
7
    if (random_number < 0.8) {
8
      setTimeout(() => {
9
        resolve(`Show weather in ${city}`);
10
      }, duration);
11
    } else {
12
      setTimeout(() => {
13
        reject(`Data unavailable for ${city}`);
14
      }, duration);
15
    }
16
  });
17
}
18
19
const promise_a = create_promise("Delhi");
20
const promise_b = create_promise("London");
21
const promise_c = create_promise("Sydney");
22
23
const my_promises = [create_promise("Delhi"), create_promise("London"), create_promise("Sydney"), create_promise("Rome"), create_promise("Las Vegas")];
24
25
async function result_from_promises(promises) {
26
  let loading_status = await Promise.allSettled(promises);
27
  console.log(loading_status);
28
}
29
30
result_from_promises(my_promises);
31
32
/* Outputs

33


34
[

35
  {

36
    "status": "fulfilled",

37
    "value": "Show weather in Delhi"

38
  },

39
  {

40
    "status": "fulfilled",

41
    "value": "Show weather in London"

42
  },

43
  {

44
    "status": "fulfilled",

45
    "value": "Show weather in Sydney"

46
  },

47
  {

48
    "status": "rejected",

49
    "reason": "Data unavailable for Rome"

50
  },

51
  {

52
    "status": "fulfilled",

53
    "value": "Show weather in Las Vegas"

54
  }

55
]

56


57
*/

As you can see, each object in our array contains a status property to let us know if the promise was fulfilled or rejected. In the case of fulfilled promises, it contains the resolved value in the value property. In the case of rejected promises, it contains the reason for rejection in the reason property.

Final Thoughts

We learned about two useful methods of the Promise class that let you work with multiple promises at once. The Promise.all() method is helpful when you want to stop waiting for other promises to settle as soon as one of them is rejected. The Promise.allSettled() method is helpful when you want to wait for all the promises to settle, regardless of their resolution or rejection status.


Converting and Transforming Arrays in JavaScript

Converting and Transforming Arrays in JavaScript

Arrays are a fundamental and powerful data structure in programming. Their power doesn't just come from their ability to store multiple objects or values. They also expose a variety of tools that make it easy to manipulate and work with the data they contain.

We often need to change an array to meet a specific need. For example, you may need to reorganize the objects in an array so that it is sorted by the value of a particular property, or you may need to merge multiple arrays into a single array. In many cases, you may need to completely transform an array of objects into another array of completely different objects.

In this tutorial, you will learn about the tools JavaScript provides to merge, copy, convert, and filter arrays. Before we begin, however, it is important that I point out that while I use the terms "merge", "convert", "transform", and "filter", very rarely do these processes change an existing array. Instead, they create a new array that contains the merged, converted, transformed, and filtered data—leaving the original array in its unchanged and pristine format.

Merging Arrays

Perhaps you are working with data that comes from different sources, or you may have multiple arrays and want to combine them into a single array to make it easier to process them. Regardless of your reasons, sometimes you need to combine multiple arrays into a single array. JavaScript gives us two ways to combine arrays. You can either use the concat() method or the spread operator (...).

The concat() method is used to merge two or more arrays and returns a new array containing the elements of the joined arrays. The new array will first be populated by the elements in the array object on which you call the method. It will then be populated by the elements of the array objects you pass to the method. For example:

1
const array1 = [1, 2, 3];
2
const array2 = [4, 5, 6];
3
const mergedArray = array1.concat(array2);
4
console.log(mergedArray); // output: [1, 2, 3, 4, 5, 6]

In this code, we have two arrays, array1 and array2. We merge these arrays into a new array called mergedArray using the concat() method, and you can see the resulting array contains the elements [1, 2, 3, 4, 5, 6].  The example below alters the code so that the concat() method is called on array2:

1
const array1 = [1, 2, 3];
2
const array2 = [4, 5, 6];
3
const mergedArray2 = array2.concat(array1);
4
console.log(mergedArray2); // output: [4, 5, 6, 1, 2, 3]

Notice that in this code, the elements in the resulting array are in a different order: [4, 5, 6, 1, 2, 3]. So, if element order is important to you, be sure to use concat() in your desired order. 

The spread operator, on the other hand, allows you to expand the elements of an array, and it can be used within a new array literal to merge arrays. For example:

1
const array1 = [1, 2, 3];
2
const array2 = [4, 5, 6];
3
const mergedArray = [...array1, ...array2];
4
console.log(mergedArray); // output: [1, 2, 3, 4, 5, 6]

Here, we again have two arrays, array1 and array2, but we merge them into a new array called mergedArray using the spread operator. The end result is the same as the first concat() example, but using this approach gives you (and those reading your code) a clearer understanding of how the mergedArray is built and populated.

Copying Arrays

There are several reasons why you may want to copy an array. You may want to preserve an array's original data (if they are simple values), or you may want to avoid any unintended side effects of working with or manipulating an array object itself. Regardless of the reason, JavaScript makes it very easy to create a copy of an array.

To create a copy of an array, you can use the slice() method. This method returns a shallow copy (more on that later) of the array you call it on. For example:

1
const originalArray = [1, 2, 3, 4, 5];
2
const copiedArray = originalArray.slice();
3
4
console.log(copiedArray); // output: [1, 2, 3, 4, 5]

This code defines an array called originalArray, and we create a copy of it using the slice() method without passing any arguments. The copiedArray object contains the same values as the original, but it is a completely different array object.

You can also use the slice() method to extract a portion of an array by specifying the start and end indices.

1
const originalArray = [1, 2, 3, 4, 5];
2
const slicedArray = originalArray.slice(1, 4);
3
4
console.log(slicedArray); // output: [2, 3, 4]

In this example, we create a sliced array that contains the elements from index 1 to index 3 (the end index passed to the slice() method is not included) of the original array.

What Is a Shallow Copy?

A shallow copy refers to creating a new object or array that is a copy of the original object or collection, but only at the first level. In other words, a shallow copy duplicates the structure of the original object, but not the objects or elements contained within it.

When you create a shallow copy of an array, the new array will have its own set of references to the same objects or elements as the original array. This means that if the original array contains simple values (e.g. numbers, strings, or booleans), the shallow copy will effectively create a new array with the same values. However, if the original array contains objects or other reference types (such as other arrays or objects), the shallow copy will only copy the references to those objects—not the objects themselves. As a result, any changes made to the objects within the original array will also be reflected in the shallow copy and vice versa, since they still refer to the same objects in memory.

In contrast, a deep copy creates a new object or collection that is a complete, independent copy of the original object or collection, including all the nested objects or elements. This means that changes made to the objects within the original array will not affect the deep copy, and vice versa, as they have their own set of objects in memory.

Here's an example to illustrate the difference:

1
const originalArray = [1, 2, { a: 3 }];
2
const shallowCopy = originalArray.slice();
3
const deepCopy = JSON.parse(JSON.stringify(originalArray));
4
5
originalArray[2].a = 4;
6
7
console.log(shallowCopy); // output: [1, 2, { a: 4 }]

8
console.log(deepCopy); // output: [1, 2, { a: 3 }]

In this example, the shallowCopy reflects the changes made to the original array, while the deepCopy remains unaffected.

Converting Arrays to Strings

Arrays are a programming construct, and there are many times when we need to convert the array into a string. Maybe we need to present an array's contents to the user. Perhaps we need to serialize the contents of an array into a format other than JSON.

By using the join() method, you can convert an array to a string. By default, the elements are separated by a comma, but you can specify a custom separator by passing a string as an argument to the join() method. For example:

1
const fruitArray = ['apple', 'banana', 'cherry'];
2
const fruitString = fruitArray.join(', ');
3
4
console.log(fruitString); // output: "apple, banana, cherry"

In this example, we have an array called fruitArray, and we convert it to a string using the join() method with a custom separator—a comma followed by a space.

A more useful example of using join() is to output a URL query string from an array that contains URL query string parameters, as shown here:

1
const queryParamsArray = [
2
  'search=JavaScript',
3
  'page=1',
4
  'sort=relevance',
5
];
6
7
const queryString = queryParamsArray.join('&');
8
9
const url = 'https://example.com/api?' + queryString;
10
console.log(url); // output: "https://example.com/api?search=JavaScript&page=1&sort=relevance"

In this code, we have an array called queryParamsArray that contains a set of query string parameters. We then use the join() method to concatenate the elements of the array with the & delimiter to form a query string. Finally, we construct the complete URL by appending the query string to the base URL.

Generating URL query parameter strings is a common use case for using join(). However, instead of simple, predefined strings as shown in this example, you'd work with an array of complex objects that you'd then have to transform into an array of strings that you can join together.

Transforming Arrays

The ability to transform an array is one of the most useful and powerful features in JavaScript. As I mentioned earlier in this tutorial, you aren't really transforming an array—you are creating a new array that contains the transformed objects or values. The original array is not modified.

To transform an array, you use the map() method. It accepts a callback function as an argument, and it executes that function for every element in the array.

1
map(function (currentElement[, index, array]));

The callback function can accept the following three arguments:

  • currentElement: the current element to transform (required)
  • index: the index of the current element (optional)
  • array: the array the map() method is called on (optional)

The callback function's return value is then stored as an element in the new array. For example:

1
const numbers = [1, 2, 3, 4, 5];
2
3
function square(number) {
4
  return number * number;
5
}
6
7
const squaredNumbers = numbers.map(square);
8
9
console.log(squaredNumbers); // output: [1, 4, 9, 16, 25]

In this code, we have an array called numbers, and we declare a function called square that takes a number as input and returns the square of that number. We pass the square function to numbers.map() to create a new array, called squaredNumbers, that contains the squared values of the original numbers.

But let's look at an example that builds a URL query string from an array of objects. The original array will contain objects that have param (for the parameter name) and value (for the parameter value) properties.

1
const queryParams = [
2
  { param: 'search', value: 'JavaScript' },
3
  { param: 'page', value: 1 },
4
  { param: 'sort', value: 'relevance' },
5
];
6
7
function createParams(obj) {
8
  return obj.param + '=' + obj.value;
9
}
10
11
const queryStringArray = queryParams.map(createParams);
12
13
const queryString = queryStringArray.join('&');
14
15
const url = 'https://example.com/api?' + queryString;
16
console.log(url); // output: "https://example.com/api?search=JavaScript&page=1&sort=relevance"

In this example, we have an array called queryParams that contains objects that we want to convert into a query string. We declare a function called createParams that accepts an object as input and returns a string in the format "param=value". Then, we create a new array called queryStringArray by applying the createParams function to each object in the original array using the map() method.

Next, we join() the queryStringArray to create the final query string, using the & delimiter to separate each param=value pair, and then we construct the complete URL by appending the query string to the base URL.

Using the map() method is a vital part of working with arrays, but sometimes we only need to work with a few elements within an array.

Filtering Arrays

The filter() method allows you to create a new array that contains only the elements that satisfy a given condition. This is achieved by passing a callback function to the filter() method which tests each element in the original array. If the callback function returns true, the element is included in the new array; if it returns false, the element is excluded.

The callback function uses the same signature as the map() method's callback function:

1
filter(function(currentElement[, index, array]));

The currentElement parameter is required, but index and array are optional. For example:

1
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
2
3
function isEven(number) {
4
  return number % 2 === 0;
5
}
6
7
const evenNumbers = numbers.filter(isEven);
8
9
console.log(evenNumbers); // output: [2, 4, 6, 8, 10]

In this example, we have an array called numbers. We declare a function called isEven that takes a number as input and returns true if the number is even (i.e. divisible by 2) or false otherwise. We create a new array called evenNumbers by filtering the original array using the isEven function as the callback function for the filter() method. The resulting evenNumbers array contains only the even numbers from the original array.

The filter() method is a powerful tool for processing arrays, allowing you to easily extract relevant data or create subsets of an array based on specific criteria.

Conclusion

Arrays are one of the most versatile and useful objects in JavaScript because we have the tools to easily merge, copy, convert, transform, and filter them. Each of these techniques serves a specific purpose, and you can combine them in various ways to effectively manipulate and process arrays in your JavaScript applications. By understanding and applying these methods, you'll be better equipped to tackle a wide range of programming challenges that involve working with arrays.

As you continue to develop your JavaScript skills, remember to practice using these array methods and explore other built-in array functions available in the language. This will help you become more proficient in JavaScript and enable you to write more efficient, clean, and maintainable code. Happy coding!