Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to chain promises on reject

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.

like image 975
FtDRbwLXw6 Avatar asked Sep 02 '17 00:09

FtDRbwLXw6


People also ask

How do you handle reject promises?

We must always add a catch() , otherwise promises will silently fail. In this case, if thePromise is rejected, the execution jumps directly to the catch() method. You can add the catch() method in the middle of two then() methods, but you will not be able to break the chain when something bad happens.

What happens when a Promise is rejected?

If the Promise rejects, the second function in your first . then() will get called with the rejected value, and whatever value it returns will become a new resolved Promise which passes into the first function of your second then.

What is a Promise chain?

Promise chaining: Promise chaining is a syntax that allows you to chain together multiple asynchronous tasks in a specific order. This is great for complex code where one asynchronous task needs to be performed after the completion of a different asynchronous task.

Does reject end Promise?

The reject function of a Promise executor changes the state of the Promise , but does not stop the executor. In general, it is recommended to add a return statement after the reject to stop unintended code execution.

Can you reject a Promise multiple times?

No. It is not safe to resolve/reject promise multiple times. It is basically a bug, that is hard to catch, becasue it can be not always reproducible.


2 Answers

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

like image 143
Meirion Hughes Avatar answered Sep 20 '22 03:09

Meirion Hughes


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.

like image 27
emil Avatar answered Sep 23 '22 03:09

emil