DEV Community

Cover image for Efficient Asynchronous Operations in JavaScript: Using Promise.all with map and for-await-of
Ajay Chavan
Ajay Chavan

Posted on

Efficient Asynchronous Operations in JavaScript: Using Promise.all with map and for-await-of

Introduction

Asynchronous operations are a cornerstone of modern JavaScript, enabling applications to handle tasks like API calls, file reading, and timers without blocking the main thread. In this post, we'll explore two powerful techniques for handling multiple asynchronous operations: Promise.all combined with map, and the for-await-of loop. By the end of this article, you'll understand when and how to use these methods to write more efficient and readable asynchronous code.

Understanding Promises and Async/Await

Before diving into the specifics, let's quickly review some core concepts:

  • Promises: These represent the eventual completion (or failure) of an asynchronous operation and its resulting value.

  • Async/Await: Syntactic sugar built on Promises that allows you to write asynchronous code that looks and behaves like synchronous code.

Using Promise.all with map

When to Use Promise.all

Promise.all is used when you need to run multiple asynchronous operations in parallel and wait for all of them to complete. This is particularly useful when the operations are independent of each other.

Example Scenario
Imagine you have an array of URLs, and you want to fetch data from all of them:

const urls = [
    'https://api.example.com/data1',
    'https://api.example.com/data2',
    'https://api.example.com/data3'
];

const fetchData = async (url) => {
    const response = await fetch(url);
    return response.json();
};

const fetchAllData = async () => {
    const promises = urls.map(url => fetchData(url));
    const results = await Promise.all(promises);
    return results;
};

fetchAllData().then(data => console.log(data)).catch(error => console.error(error));

Enter fullscreen mode Exit fullscreen mode

Explanation

  • map: Transforms the array of URLs into an array of Promises.
  • Promise.all: Waits for all Promises to resolve and returns an array of results. If any Promise is rejected, Promise.all immediately rejects with that reason.

Benefits

  • Efficiency: All requests are fired off simultaneously, reducing overall wait time.
  • Simplicity: Easy to understand and implement for parallel operations.

Using for-await-of

When to Use for-await-of

The for-await-of loop is ideal when you need to process each asynchronous operation sequentially, ensuring that one completes before starting the next. This can be necessary when operations are dependent on each other or when you need to limit concurrent operations to avoid overwhelming resources.

Example Scenario

Suppose you need to fetch data from the same array of URLs, but you want to process each response before starting the next request:

const fetchSequentially = async () => {
    for (const url of urls) {
        try {
            const response = await fetch(url);
            const data = await response.json();
            console.log(data);
        } catch (error) {
            console.error('Error fetching data:', error);
        }
    }
};

fetchSequentially();

Enter fullscreen mode Exit fullscreen mode

Explanation

  • for...of loop: Iterates over the array of URLs.
  • await: Ensures each fetch request completes before moving to the next.

Benefits

  • Order: Guarantees operations are completed in sequence.
  • Control: Allows handling of each request individually, which can be useful for error handling and resource management.

Choosing the Right Tool

  1. Use Promise.all with map when:

    • You have independent asynchronous operations.
    • You need maximum efficiency and reduced total execution time.
  2. Use for-await-of when:

    • Operations are dependent on each other.
    • You need to manage concurrency to avoid resource overuse. Sequential processing is required for correctness.

Conclusion

Both Promise.all with map and for-await-of are powerful tools in your asynchronous programming toolkit. Understanding when to use each method will help you write cleaner, more efficient, and more maintainable JavaScript code. Happy coding!

Feel free to expand on any section with additional details, examples, or personal insights to make the post uniquely yours.

Top comments (1)

Collapse
 
jimzord12 profile image
jimzord12

Very nice article! Help me a lot.

I though it would be awesome to have a helper function that handles the parrallel fetching, after a few refactors this is what I ended up with:

type FetchOptions<T> = {
    urls: string[];
    fetchFunc?: (url: string) => Promise<T>;  // Custom fetch function if needed
    onError?: (error: any, url: string) => void;  // Optional error handler
};

async function fetchInParallel<T>({
    urls,
    fetchFunc = async (url: string) => {
        const response = await fetch(url);
        if (!response.ok) {
            throw new Error(`Failed to fetch from ${url}: ${response.statusText}`);
        }
        return response.json() as Promise<T>;
    },
    onError = (error, url) => {
        console.error(`Error fetching from ${url}:`, error);
    },
}: FetchOptions<T>): Promise<T[]> {
    const promises: Promise<T>[] = urls.map(async (url) => {
        try {
            return await fetchFunc(url);
        } catch (error) {
            onError(error, url);
            throw error; // Rethrow the error to handle it in Promise.all
        }
    });

    return Promise.all(promises);
}

// Usage Example
const WEB_SERVER_BASE_URL = "http://localhost:8080/api";
const todosToFetch: number[] = [todo_Id_1, todo_Id_2, todo_Id_N];

const urlsOfTodosToFetch: string[] = todosToFetch.map((id) => 
    `${WEB_SERVER_BASE_URL}/todos/${id}`
);

fetchInParallel({
    urls: urlsOfTodosToFetch,
})
    .then((todos) => console.log("Todos: ", todos))
    .catch((error) => console.error("Failed to fetch some todos:", error));
Enter fullscreen mode Exit fullscreen mode

What do you think?