Promises first come, first serve

In JavaScript we have this thing called a Promise. A data-structure which is found in some kind of shape or form in a myriad of programming languages to handle tasks happening at some point in the future.

Promise is far from perfect but has the ability to use it as a hammer for many different problems.

When working with multiple asynchronous tasks, in JS one common approach is to use Promise.all(). It allows to run several independent operations in parallel, which can seem efficient at first glance. However, a downside of this method is that we must wait for all the initiated promises to resolve before we can begin processing any of their results.

Promise.race() solves issues in the problem set, but isn't really a definitive solution by itself.

Promise.races

So what if instead of just Promise.race, we had Promise.races :D? Where individual promises from a collection are resolved as fast as possible.

So once a promise resolves, it can be handled ASAP.

This can be very well implemented, though. Consider:

async function* racePromises<T>(promises: Promise<T>[]) {
  const pool = new Map(
    promises.map((promise, i) => [
      i,
      promise.then((value) => [i, value] as const)
    ])
  )

  while (pool.size) {
    const [key, value] = await Promise.race(pool.values())
    pool.delete(key)
    yield value
  }
}

The implementation might look a bit intimidating, but in essence: boom -- promises, handled as soon as they resolve.

for await (const x of racePromises(someApiCallPromises)) {
  // handle each result as soon as it's available,
  // instead of waiting for all to resolve with something like Promise.all(someApiCallPromises)
}

Growing generators

Having racePromises be a generator is obviously not "optimal optimal" as commonly JavaScript developers do not really play with generators unless they really have to.

But the generator / iterators are gaining popularity, as can be seen from the nice set of landed and upcoming ECMAScript proposals (such as iterator helpers, async iterator helpers and joint iteration).

Promise.settle

But back to the issue at hand. There is one thing to solve with the first come, first serve Promise handling -- arising errors. When this kind of first come, first-serve use case is called for, it is highly likely that the underlying promises very well may throw.

In that case the generator can simply be composed further with familiar JavaScript promise semantics straight from Promise.allSettled().

Creating an asSettled function which wraps a promise to the PromiseSettledResult interface and composing it with the racePromises.

const asSettled = <T>(promise: Promise<T>) =>
  promise
    .then((value) => ({
      status: 'fulfilled' as const,
      value
    }))
    .catch((reason) => ({
      status: 'rejected' as const,
      reason
    }))

const racePromisesSettled = <T>(promises: Promise<T>[]) =>
  racePromises(promises.map(asSettled))

Proper functional ergonomics with proper speed!

for await (const result of racePromisesSettled(someApiCallPromises)) {
  if (result.status === 'rejected') {
    // handle error from result.reason
    continue
  }
  // do something with result.value
}

The asSettled function itself opens up Go-style error handling -- or in other words it allows errors as values. A similar theme as the try operator proposal is pushing toward.

But instead of adding the functionality with new syntax, we are just using existing JavaScript semantics (PromiseSettledResult).

The DX potential is already there. It just needs to be reached for.