6

Given a function, fn, which returns a promise, and an arbitrary length array of data (e.g. data = ['apple', 'orange', 'banana', ...]) how do you chain function calls on each element of the array in sequence, such that if fn(data[i]) resolves, the whole chain completes and stops calling fn, but if fn(data[i]) rejects, the next call fn(data[i + 1]) executes?

Here is a code example:

// this could be any function which takes input and returns a promise
// one example might be fetch()
const fn = datum =>
  new Promise((resolve, reject) => {
    console.log(`trying ${datum}`);

    if (Math.random() < 0.25) {
      resolve(datum);
    } else {
      reject();
    }
  });

const foundResult = result => {
  // result here should be the first value that resolved from fn(), and it
  // should only be called until the first resolve()
  console.log(`result = ${result}`);
};

// this data can be purely arbitrary length
const data = ['apple', 'orange', 'banana', 'pineapple', 'pear', 'plum'];

// this is the behavior I'd like to model, only for dynamic data
fn('apple').then(foundResult)
  .catch(() => {
    fn('orange').then(foundResult)
      .catch(() => {
        fn('banana').then(foundResult)
          .catch(() => {
            /* ... and so on, and so on ... */
          });
      });
  });

I feel like maybe there's an elegant solution to this pattern that I'm missing. The behavior is very similar to Array.some(), but I've come up empty trying to fiddle with that.

EDIT: I switched from numeric data to string to stress that the solution needs to not be reliant on the data being numeric.

EDIT #2: Just to clarify further, fn could be any function that accepts input and returns a promise. The fn implementation above was just to give a complete example. In reality, fn could actually be something like an API request, a database query, etc.

FtDRbwLXw6
  • 27,774
  • 13
  • 70
  • 107
  • But `fn` always returns something on resolved, right? – emil Sep 02 '17 at 01:18
  • @emil Not necessarily, no. In the case where a promise resolves with no data, then `result` inside `foundResult()` will simply be `undefined`, which is fine. – FtDRbwLXw6 Sep 02 '17 at 01:29

5 Answers5

5

You could use async/await and a loop:

async function search() {
  for (let item of data) {
    try {
      return await fn(item);
    } catch (err) { }
  }
  throw Error ("not found"); 
}

search().then(foundResult).catch(console.log);
  • fn can return either Promise (awaited) or simply a value (returned)
  • your data could be an infinite iterable sequence (generator)
  • in my opinion, its also easy to read and understand intent.

here is the output if the sequence fails:

trying apple
trying orange
trying banana
trying pineapple
trying pear
trying plum
Error: not found

support for async is native in es2017, but can be transpiled to es3/es5 with babel or typescript

Meirion Hughes
  • 24,994
  • 12
  • 71
  • 122
  • This is a very elegant solution, but it still calls `foundResult` even when no promises resolve. I'm currently playing with it to try and see if I can fix that. – FtDRbwLXw6 Sep 02 '17 at 01:39
  • yup, just throw at the end. – Meirion Hughes Sep 02 '17 at 01:45
  • This passes all my tests, and I agree with you that the intent is made very clear. It even opens up the possibility of easily accumulating any rejection values inside the `catch` and including them in the resulting `throw`. Very nice! – FtDRbwLXw6 Sep 02 '17 at 01:50
  • Nice solution. I am pretty surprised no one tried function generator. – emil Sep 08 '17 at 00:42
4

You can use Array.reduce to get the desired data.

data.reduce((promise, item) => promise.then(
  (param) => {
    if (param) return Promise.resolve(param);
    return fn(item).catch(() => Promise.resolve());
  } 
), Promise.resolve())
.then(foundResult)

Basically it will pass over the result to the end once passes. And if fn is failed, it will pass over undefined valued promise to next chain to trigger fn.

emil
  • 6,074
  • 4
  • 30
  • 38
2

Write a search function like below:

function search(number) {
    if (number < data.length) {
        fn(data[number]).then(foundResult)
              .catch(() => search(number + 1));
    }
}

search(0);
Daniel Tran
  • 6,083
  • 12
  • 25
1

You could write a very simple recursive function that will stop on the first resolve and recurse on the catch.

function find_it(arr) {
   let [head, ...rest] = arr

   if (!head) return console.log("not found") // all rejects or no data

   fn(head)
   .then(r => foundResult(r) )
   .catch(r => find_it(rest))
}
find_it(data)

This has the benefit of stopping on the first match without calling all the values if a match is found and not caring about the length of data unless you exceed the stack size in the recursion. Of course it would be easy to modify the action on the edge case when all promises reject to do something.

Result when found:

$ node ./test
trying apple
trying orange
result = orange

and not found:

$ node ./test trying apple
trying orange
trying banana
trying pineapple
trying pear
trying plum
not found

Mark
  • 90,562
  • 7
  • 108
  • 148
0

What you are trying to do could be done like this I guess (it is not strictly equivalent, see reserves below):

const fn = n => new Promise(() => {});
const seq = [];
const process = n => fn(n).then(foundResult);

seq.slice(1).reduce((operation, item) => {

    return operation
        .catch(() => process(item));

}, process(seq[0]));

With Promise.race :

const fetchAll = ( urls = [] ) => Promise.race(urls.map(fn)).then(foundResult);

However I am not sure this is what you are trying to achieve : for instance in your snippet you are not returning anything in your catch handler which means that the foundResult is most likely a side effect. Moreover when reading your code snippet you are catching errors that can be raised from inside the foundResult and not from the fn function.

As a rule of thumb I try to always have my promises fulfilled by a known "type" or rejected with an error. It's not clear in your example that the promise you are generating will be settled with any value, whether it's a rejectionValue or a fulfillmentValue.

Perhaps if you would provide a use case I could help you a bit.

adz5A
  • 2,012
  • 9
  • 10
  • The behavior in the code works as I need it to work, but it's hard-coded (and non-exhaustive), where it needs to be dynamic, as the elements in `data` are not set in stone. As far as a use-case goes, one example might be where `data` is a list of URLs, and `fn` is actually `fetch`, and you want the HTTP response from the first URL in the array that doesn't fail, walking them in the order that they appear in the array, one at a time. – FtDRbwLXw6 Sep 02 '17 at 00:50
  • I updated, you most likely want to use Promise.race. Its semantics are easy, given an iterable of promises, it returns a promise settled with the first settled values of this iterable. – adz5A Sep 02 '17 at 00:59
  • `Promise.race` will execute `fn` for every element in `data` without regard to whether the previous elements resolved or rejected, though. In the case of URLs, that means that it'll fire off `data.length` requests asynchronously, and then resolve with whichever response comes back first. What I need is for it to fire off a request for `data[0]`, wait for the response, if it succeeded then resolve, else fire off a request for `data[1]`, and so on. – FtDRbwLXw6 Sep 02 '17 at 01:10