bosticko bosticko - 3 months ago 5
Javascript Question

Finding first success amongst Promise returning functions

First off, given some number of functions:

function foo(arg) {
if (stuff(arg)) {
return 'result from foo';
} else {
return null;
}
};

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

function bar(arg) {
if (otherStuff(arg)) {
return 'result from bar';
} else {
return null;
}
};


How can the functions be executed serially, until one returns a result, in a short-circuit fashion, returning the result of that function?

[
foo,
// ...
bar
].firstWithArg('some arg');
// => 'result from ___', or `null`


I thought
Array.prototype.find
might be what I want, but it would return the function, not its result.

Essentially, the desired behaviour is:

foo('some arg') || ... || bar('some arg');





Next, how can this behaviour be extended, if the functions return promises?

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 the same behaviour be implemented? Successively calling each function, with the same arguments, short-circuiting after we reach the first one to resolve with 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.

Answer

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.