32

It seems I'm unable to use an async function as the first argument to Array.find(). I can't see why this code would not work what is happening under the hood?


function returnsPromise() {
  return new Promise(resolve => resolve("done"));
}

async function findThing() {
  const promiseReturn = await returnsPromise();
  return promiseReturn;
}

async function run() {
  const arr = [1, 2];
  const found = await arr.find(async thing => {
    const ret = await findThing();
    console.log("runs once", thing);
    return false;
  });
  console.log("doesn't wait");
}

run();

https://codesandbox.io/s/zk8ny3ol03

Laurie Poulter
  • 574
  • 2
  • 5
  • 10

6 Answers6

35

Simply put, find does not expect a promise to be returned, because it is not intended for asynchronous things. It loops through the array until one of the elements results in a truthy value being returned. An object, including a promise object, is truthy, and so the find stops on the first element.

If you want an asynchronous equivalent of find, you'll need to write it yourself. One consideration you'll want to have is whether you want to run things in parallel, or if you want to run them sequentially, blocking before you move on to the next index.

For example, here's a version that runs them all in parallel, and then once the promises are all resolved, it finds the first that yielded a truthy value.

async function findAsync(arr, asyncCallback) {
  const promises = arr.map(asyncCallback);
  const results = await Promise.all(promises);
  const index = results.findIndex(result => result);
  return arr[index];
}

//... to be used like:

findAsync(arr, async (thing) => {
  const ret = await findThing();
  return false;
})
Nicholas Tower
  • 72,740
  • 7
  • 86
  • 98
16

Here is a TypeScript version that runs sequentially:

async function findAsyncSequential<T>(
  array: T[],
  predicate: (t: T) => Promise<boolean>,
): Promise<T | undefined> {
  for (const t of array) {
    if (await predicate(t)) {
      return t;
    }
  }
  return undefined;
}
Sebastien Lorber
  • 89,644
  • 67
  • 288
  • 419
0

It might help you to note that Array.prototype.filter is synchronous so it doesn't support async behaviour. I think that the same applies to the "find" property. You can always define your own async property :) Hope this helps!

0

The other answers provide two solutions to the problem:

  1. Running the promises in parallel and awaiting all before returning the index
  2. Running the promises sequencially, so awaiting every single promise before moving on to the next one.

Imagine you have five promises that finish at different times: The first after one second, the second after two seconds, etc... and the fifth after five seconds.

If I'm looking for the one that finished after three seconds:

  1. The first solution will wait 5 seconds, until all promises are resolved. Then it looks for the one that matches.
  2. The second one will evaluate the first three matches (1 + 2 + 3 = 6 seconds), before returning.

Here's a third option that should usually be faster: Running the promises in parallel, but only waiting until the first match ("racing them"): 3 seconds.

function asyncFind(array, findFunction) {
  return new Promise(resolve => {
    let i = 0;
    array.forEach(async item => {
      if (await findFunction(await item)) {
        resolve(item);
        return;
      }
      i++;
      if (array.length == i) {
        resolve(undefined);
      }
    });
  });
}

//can be used either when the array contains promises
var arr = [asyncFunction(), asyncFunction2()];
await asyncFind(arr, item => item == 3);

//or when the find function is async (or both)
var arr = [1, 2, 3];
await asyncFind(arr, async item => {
    return await doSomething(item);
}

When looking for a non-existant item, solutions 1 and 3 will take the same amount of time (until all promises are evaluated, here 5 seconds). The sequencial approach (solution 2) would take 1+2+3+4+5 = 15 seconds.

Demo: https://jsfiddle.net/Bjoeni/w4ayh0bp

Bjoeni
  • 1
0

I came up with a solution that doesn't appear to be covered here, so I figured I'd share. I had the following requirements.

  • Accepts an async function (or a function that returns a promise)
  • Supplies item, index, and array to the function
  • Returns the item with the fastest resolved promise (all are evaluated in parallel)
  • Does not exit early if the async function rejects (throws an error)
  • Supports generic types (in TypeScript)

With those requirements, I came up with the following solution using Promise.any. It will resolve with the first fulfilled value or reject with an AggregateError if none of the promises are fulfilled.

type Predicate<T> = (item: T, index: number, items: T[]) => Promise<boolean>

export const any = async <T>(array: T[], predicate: Predicate<T>): Promise<T> => {
  return Promise.any(
    array.map(async (item, index, items) => {
      if (await predicate(item, index, items)) {
        return item
      }

      throw new Error()
    })
  )
}

Now we can search an array in parallel with async functions and return the fastest resolved result.

const things = [{ id: 0, ... }, { id: 1, ... }]
const found = await any(things, async (thing) => {
  const otherThing = await getOtherThing()

  return thing.id === otherThing.id
})

Enumerate the array sequentially

If we want to enumerate the array in sequence, as Array.find does, then we can do that with some modifications.

export const first = async <T>(array: T[], predicate: Predicate<T>): Promise<T> => {
  for (const [index, item] of array.entries()) {
    try {
      if (await predicate(item, index, array)) {
        return item
      }
    } catch {
      // If we encounter an error, keep searching.
    }
  }

  // If we do not find any matches, "reject" by raising an error.
  throw new Error()
}

Now, we can search an array in sequence and return the first resolved result.

const things = [{ id: 0, ... }, { id: 1, ... }]
const found = await first(things, async (thing) => {
  const otherThing = await getOtherThing()

  return thing.id === otherThing.id
})
davidmyersdev
  • 1,042
  • 8
  • 10
0

All suggestions so far are missing the second parameter of find. Therefore I provide two further suggestions:

Array.prototype.asyncFind = function (predicate, thisArg=null) {
    return Promise.any(this.map(async (value, index, array) =>
        new Promise(async (resolve, reject) => 
            await predicate.bind(thisArg)(value, index, array) ? resolve(value) : reject()
        )
    )).catch(() => undefined)
}

The first one runs in parallel and returns (just like the original) the first value (with respect to time) that satisfies the provided testing function or undefined if none is found.

Array.prototype.asyncFind = async function (predicate, thisArg=null) {
    const boundPredicate = predicate.bind(thisArg)

    for (const key of this.keys()) {
        if (await boundPredicate(this[key], key, this)) return this[key]
    }

    return undefined
}

The second one runs one after another (taking sorting into account) and returns (just like the original) the first value (with respect to sorting) that satisfies the provided testing function or undefined if none is found.

Koudela
  • 181
  • 6