Chain async fallbacks and print their errors only if all fail

Viewed 39

I want to invoke an async method, followed by a series of async fallback methods until one of them succeeds.
If all invocations fail, then I want all of their errors printed. Otherwise, if even one succeeds, the errors should not be printed.
This is what I want:

tryX()
 .catch(x => tryXFallback1()
  .catch(xf1 => tryXFallback2()
   .catch(xf2 => tryXFallback3()
    .catch(xf3 => tryXFallback4()
     // ...
     .catch(xf4 => Promise.reject([x, xf1, xf2, xf3, xf4]))))));

But I'm not a fan of the indentation. Accumulating the errors in a variable outside the scope of the catch clauses also seems messy:

let errors = [];
tryX()
    .catch(x => {
        errors.push(x);
        return tryXFallback1();
    })
    .catch(xf1 => {
        errors.push(x);
        return tryXFallback2();
    })
    .catch(xf2 => {
        errors.push(x);
        return tryXFallback3();
    })
    .catch(xf3 => {
        errors.push(x);
        return tryXFallback4();
    })
    // ...
    .catch(xf4 => Promise.reject(errors));

Lastly, I thought I could do some sort of for loop instead but that seems even uglier e.g.:

let methods = [tryX, tryFallback1, tryFallback2, tryFallback3, tryFallback4, /*...*/];
let errors = [];
for (let x of methods)
    try {
        return await x();
    } catch (e) {
        errors.push(e);
    }
if (errors.length === methods.length)
    return Promise.reject(errors);

Does anyone know of a more elegant approach?

2 Answers

The loop you have seems fine. I would probably stick with it as it already works. However, here is an alternative:

function tryWithFallbacks(main, ...fallbacks) {
  return fallbacks.reduce(
    (p, nextFallback) => p.catch( //handle errors
      err => nextFallback() //try using the fallback
        .catch(e => Promise.reject(err.concat(e))) //propagate rejection reasons
                                                   //on failure by adding to 
                                                   //the array of errors
    ), 
    main() //seed the process with the main task
      .catch(err => Promise.reject([err])) //ensure there is an array of errors
  );
}

const a = tryWithFallbacks(
  () => Promise.resolve(42)
);
test(a, "a"); //42

const b = tryWithFallbacks(
  () => Promise.reject("oops"),
  () => Promise.resolve("it's fine")
);
test(b, "b"); //"it's fine"

const c = tryWithFallbacks(
  () => Promise.reject("oops1"),
  () => Promise.reject("oops2"),
  () => Promise.reject("oops3")
);
test(c, "c"); //["oops1", "oops2", "oops3"]

const d = tryWithFallbacks(
  () => Promise.reject("oops1"),
  () => Promise.reject("oops2"),
  () => Promise.reject("oops3"),
  () => Promise.resolve("finally!")
);
test(d, "d"); //"finally!"

const e = tryWithFallbacks(
  () => Promise.reject("oops1"),
  () => Promise.reject("oops2"),
  () => Promise.reject("oops3"),
  () => Promise.resolve("penultimate try successful!"),
  () => Promise.reject("this is not reached")
);
test(e, "e"); //"penultimate try successful!"

//a simple function to illustrate the result
function test(promise, name) {
  promise
    .then(result => console.log(`[${name}] completed:`, result))
    .catch(errorResult => console.log(`[${name}] failed:`, errorResult));
}

It's Array#reduce()-ing all the promises into one and making sure of the sequential order. If any succeed, you just get a single successful result. On failure, the error response is added to an array of all errors and passed forward via the rejection flow of promises.

This currently does require that all the functions that produce a promise are thunks - they take no input.

For reference, an equivalent operation using await and a loop would be:

async function tryWithFallbacks(...tasks) {
  const errors = [];
  
  for (const task of tasks) {
    try {
      const result = await task();
      return result;
    } catch (err) {
      errors.push(err);
    }
  }
  
  return errors;
}

const a = tryWithFallbacks(
  () => Promise.resolve(42)
);
test(a, "a"); //42

const b = tryWithFallbacks(
  () => Promise.reject("oops"),
  () => Promise.resolve("it's fine")
);
test(b, "b"); //"it's fine"

const c = tryWithFallbacks(
  () => Promise.reject("oops1"),
  () => Promise.reject("oops2"),
  () => Promise.reject("oops3")
);
test(c, "c"); //["oops1", "oops2", "oops3"]

const d = tryWithFallbacks(
  () => Promise.reject("oops1"),
  () => Promise.reject("oops2"),
  () => Promise.reject("oops3"),
  () => Promise.resolve("finally!")
);
test(d, "d"); //"finally!"

const e = tryWithFallbacks(
  () => Promise.reject("oops1"),
  () => Promise.reject("oops2"),
  () => Promise.reject("oops3"),
  () => Promise.resolve("penultimate try successful!"),
  () => Promise.reject("this is not reached")
);
test(e, "e"); //"penultimate try successful!"

//a simple function to illustrate the result
function test(promise, name) {
  promise
    .then(result => console.log(`[${name}] completed:`, result))
    .catch(errorResult => console.log(`[${name}] failed:`, errorResult));
}

Use Promise.any():

Promise.any([tryXFallback1, tryXFallback2, tryXFallback3]).then(result => console.log(result)).catch(err => console.log(err.errors))

This solves everything you need:

  1. Works if any async of the passed array is resolved
  2. Logs all of the errors that were thrown (The object that is passed to the catch callback of any, has property .errors)
Related