Async collection methods

Proposal: Asynchronous Array Methods
GitHub Proposal Link: tc39/proposal-{{name-here}}

Introduction

A prevalent pattern in modern JavaScript involves transforming arrays into arrays of promises to handle asynchronous operations effectively. Consider the following common pattern:

const array2 = await Promise.all(array1.map(asyncFn));

While this pattern works well with map(), it doesn't extend easily to other Array methods like filter(), every(), or some() when asynchronous predicates are involved. This leads to cumbersome and less intuitive workarounds.

Terminology

  • Collection Functions: Methods on the Array object that use a callback to manipulate data, e.g., Array.filter, Array.map.
  • Predicate: The callback function used in collection methods.

Motivation

Here are some current ways to handle asynchronous predicates in array methods:

Verbose Async Filtering:

const filteredAsync = await Promise.all(array.map(async (item) => {
  const keep = await asyncPredicate(item);
  return keep ? item : null; // null or a sentinel value for filtering.
})).filter(item => item != null);

Synchronous Loop with Asynchronous Checks:

const results = [];
for (let item of array) {
  if (await asyncPredicate(item)) {
    results.push(item);
  }
}

Proposed Ideas

I propose considering the following enhancements to improve handling asynchronous operations in Array methods:

Idea 1: Automatic Promise Detection

Detect when a predicate returns a Promise and adjust the method’s return type to Promise, affecting its behavior.

Example: Automatic Detection in map()

const array2 = await array1.map(asyncPredicate); // Detects async and awaits internally.

Pros:

  • Intuitive and clean for new code bases.

Cons:

  • Breaks existing expectations where an array of Promises is anticipated (e.g., Promise.race()).

Idea 2: Optional Parameter for Async Mode

Introduce an additional parameter that specifies asynchronous operations without altering the signature of existing parameters, such as thisArg.

Example: Optional Async Parameter in filter() with thisArg

const asyncFiltered = await array1.filter(asyncPredicate, null, { async: true });

Pros:

  • Backward compatible.
  • Does not alter the expected behavior of the thisArg.

Cons:

  • Slightly more complex function signatures
  • You can't anticipate the expected behaviour until after reading the async arg. In code where there may be a long and complex async function inlined to do something like filtering, this affects clarity.

Idea 3: Dedicated Asynchronous Methods

Add new asynchronous counterparts like asyncMap, asyncFilter, which inherently support asynchronous operations.

Example: Dedicated Asynchronous map()

const processedArray = await array1.asyncMap(asyncPredicate);

Pros:

  • Explicit and declarative.
  • Avoids altering existing method behaviors.

Cons:

  • Increases the API surface of Array.

Conclusion

I lean towards Idea 3 after consideration, as it offers clarity and backward compatibility. Whichever approach is chosen, It would be important to decide the most efficient asynchronous handling for each method, such as the parallel execution and cancellation mechanisms for predicates that no longer need evaluation.

Feel free to share your thoughts or suggest improvements to these proposals.

Nevermind this, I can see GitHub - tc39/proposal-async-iterator-helpers: Methods for working with async iterators in ECMAScript would resolve the issue!

1 Like

This also seems related