Search code examples
javascriptnode.jsasynchronouspromiseshort-circuiting

Finding first success amongst Promise returning functions


Given some number of functions, returning promises:

function foo(arg) {
  return new Promise(function(resolve, reject) {
    if (stuff(arg)) {
      resolve('result from foo');
    } else {
      resolve(null);
    }
  });
);

// ... maybe more of these functions ...

function bar(arg) {
  return new Promise(function(resolve, reject) {
    if (otherStuff(arg)) {
      resolve('result from bar');
    } else {
      resolve(null);
    }
  });
);

How can we iterate through the functions in a serial fashion, short circuiting after the first function returning a non-null value?

[
  foo,
  // ...
  bar
].firstWithArg('some arg')
  .then(function(result) {
    // result: 'result from ___', or `null`
  });

Essentially, the desired behaviour is:

new Promise(function(resolve, reject){
  foo('some-arg')
    .then(function(result) {
      if (result) {
        resolve(result);
      } else {

        // ...

          bar('some-arg')
            .then(function(result) {
              if (result) {
                resolve(result);
              } else {
                resolve(null); // no functions left
              }
            })
      }
    });
});

Promise.race() can't be used, as the functions can't all be fired. They must be executed serially, stopping after the first success.


Solution

  • You've said your first question is really just setup for the second, which is the real question.

    So I think your question is: How do you execute a series of functions that return promises serially, short-circuiting when the first one resolves with a non-null value?

    I probably wouldn't, I'd use reject rather than resolve(null) (but in a comment you've clarified you want resolve(null), and I see your point; I cover that below):

    function foo(arg) {
      return new Promise(function(resolve, reject) {
        if (stuff(arg)) {
          resolve('result from foo');
        } else {
          reject();          // <=== Note
        }
      });
    }
    
    // ... maybe more of these functions ...
    
    function bar(arg) {
      return new Promise(function(resolve, reject) {
        if (otherStuff(arg)) {
          resolve('result from bar');
        } else {
          reject();          // <=== Note
        }
      });
    }
    

    Then you use catch to handle rejections up until you get back a resolution:

    foo("a")
      .catch(() => bar(1))
      .catch(() => foo("b"))
      .catch(() => bar(2))
      .catch(() => foo("c"))
      .catch(() => bar(3))
      .then(value => {
        console.log("Done", value);
      });
    

    function otherStuff(arg) {
      return arg == 2;
    }
    
    function stuff(arg) {
      return arg == "c";
    }
    
    function foo(arg) {
      console.log("foo:", arg);
      return new Promise(function(resolve, reject) {
        if (stuff(arg)) {
          console.log("foo:", arg, "resolving");
          resolve('result from foo');
        } else {
          console.log("foo:", arg, "rejecting");
          reject(); // <=== Note
        }
      });
    }
    
    // ... maybe more of these functions ...
    
    function bar(arg) {
      console.log("bar:", arg);
      return new Promise(function(resolve, reject) {
        if (otherStuff(arg)) {
          console.log("bar:", arg, "resolving");
          resolve('result from bar');
        } else {
          console.log("bar:", arg, "rejecting");
          reject(); // <=== Note
        }
      });
    }
    
    foo("a")
      .catch(() => bar(1))
      .catch(() => foo("b"))
      .catch(() => bar(2))
      .catch(() => foo("c"))
      .catch(() => bar(3))
      .then(value => {
        console.log("Done", value);
      });

    That works because resolutions bypass the catch handlers, so the subsequent functions are never called.

    If you have an array of functions to call, there's an idiom for it: Array#reduce:

    let functions = [
      () => foo("a"),
      () => bar(1),
      () => foo("b"),
      () => bar(2),
      () => foo("c"),
      () => bar(3)
    ];
    
    functions.reduce((p, fn) => p.catch(fn), Promise.reject())
      .then(value => {
        console.log("Done", value);
      });
    

    function otherStuff(arg) {
      return arg == 2;
    }
    
    function stuff(arg) {
      return arg == "c";
    }
    
    function foo(arg) {
      console.log("foo:", arg);
      return new Promise(function(resolve, reject) {
        if (stuff(arg)) {
          console.log("foo:", arg, "resolving");
          resolve('result from foo');
        } else {
          console.log("foo:", arg, "rejecting");
          reject(); // <=== Note
        }
      });
    }
    
    // ... maybe more of these functions ...
    
    function bar(arg) {
      console.log("bar:", arg);
      return new Promise(function(resolve, reject) {
        if (otherStuff(arg)) {
          console.log("bar:", arg, "resolving");
          resolve('result from bar');
        } else {
          console.log("bar:", arg, "rejecting");
          reject(); // <=== Note
        }
      });
    }
    
    let functions = [
      () => foo("a"),
      () => bar(1),
      () => foo("b"),
      () => bar(2),
      () => foo("c"),
      () => bar(3)
    ];
    
    functions.reduce((p, fn) => p.catch(fn), Promise.reject())
      .then(value => {
        console.log("Done", value);
      });

    As you probably know, Array#reduce is useful for "reducing" an array to a value, such as with a simple sum:

    [1, 2, 3].reduce((sum, value) => sum + value, 0); // 6
    

    In the above, for the "sum" equivalent, we start with a rejected promise and use catch to create the chain of promises. The result of calling reduce is the last promise from catch.


    But, if you want to use resolve(null) instead, you use then in a similar way:

    foo("a")
      .then(result => result ? result : bar(1))
      .then(result => result ? result : foo("b"))
      .then(result => result ? result : bar(2))
      .then(result => result ? result : foo("d"))
      .then(result => result ? result : bar(3))
      .then(value => {
        console.log("Done", value);
      });
    

    function otherStuff(arg) {
      return arg == 2;
    }
    
    function stuff(arg) {
      return arg == "c";
    }
    
    function foo(arg) {
      console.log("foo:", arg);
      return new Promise(function(resolve, reject) {
        if (stuff(arg)) {
          console.log("foo:", arg, "resolving");
          resolve('result from foo');
        } else {
          console.log("foo:", arg, "resolving null");
          resolve(null);
        }
      });
    }
    
    // ... maybe more of these functions ...
    
    function bar(arg) {
      console.log("bar:", arg);
      return new Promise(function(resolve, reject) {
        if (otherStuff(arg)) {
          console.log("bar:", arg, "resolving");
          resolve('result from bar');
        } else {
          console.log("bar:", arg, "resolving null");
          resolve(null);
        }
      });
    }
    
    foo("a")
      .then(result => result ? result : bar(1))
      .then(result => result ? result : foo("b"))
      .then(result => result ? result : bar(2))
      .then(result => result ? result : foo("d"))
      .then(result => result ? result : bar(3))
      .then(value => {
        console.log("Done", value);
      });

    Or with an array:

    let functions = [
      () => foo("a"),
      () => bar(1),
      () => foo("b"),
      () => bar(2),
      () => foo("c"),
      () => bar(3)
    ];
    
    functions.reduce((p, fn) => p.then(result => result ? result : fn()), Promise.resolve(null))
      .then(value => {
        console.log("Done", value);
      });
    

    function otherStuff(arg) {
      return arg == 2;
    }
    
    function stuff(arg) {
      return arg == "c";
    }
    
    function foo(arg) {
      console.log("foo:", arg);
      return new Promise(function(resolve, reject) {
        if (stuff(arg)) {
          console.log("foo:", arg, "resolving");
          resolve('result from foo');
        } else {
          console.log("foo:", arg, "resolving null");
          resolve(null);
        }
      });
    }
    
    // ... maybe more of these functions ...
    
    function bar(arg) {
      console.log("bar:", arg);
      return new Promise(function(resolve, reject) {
        if (otherStuff(arg)) {
          console.log("bar:", arg, "resolving");
          resolve('result from bar');
        } else {
          console.log("bar:", arg, "resolving null");
          resolve(null);
        }
      });
    }
    
    let functions = [
      () => foo("a"),
      () => bar(1),
      () => foo("b"),
      () => bar(2),
      () => foo("c"),
      () => bar(3)
    ];
        
    functions.reduce((p, fn) => p.then(result => result ? result : fn()), Promise.resolve(null))
      .then(value => {
        console.log("Done", value);
      });

    That works because if we get back a truthy value (or you could use result => result !== null ? result : nextCall()), we return that result down the chain, which means that that then returns a resolved promise with that value; but if we get back a falsy value, we call the next function and return its promise.

    As you can see, this is a bit more verbose, which is part of why promises have this distinction between resolution and rejection.