La respuesta de Benjamin ofrece una gran abstracción para resolver este problema, pero esperaba una solución menos abstracta. La forma explícita de resolver este problema es simplemente recurrir .catch
a las promesas internas y devolver el error de su devolución de llamada.
let a = new Promise((res, rej) => res('Resolved!')),
b = new Promise((res, rej) => rej('Rejected!')),
c = a.catch(e => { console.log('"a" failed.'); return e; }),
d = b.catch(e => { console.log('"b" failed.'); return e; });
Promise.all([c, d])
.then(result => console.log('Then', result)) // Then ["Resolved!", "Rejected!"]
.catch(err => console.log('Catch', err));
Promise.all([a.catch(e => e), b.catch(e => e)])
.then(result => console.log('Then', result)) // Then ["Resolved!", "Rejected!"]
.catch(err => console.log('Catch', err));
Dando un paso más allá, podría escribir un controlador genérico de captura que se vea así:
const catchHandler = error => ({ payload: error, resolved: false });
entonces puedes hacer
> Promise.all([a, b].map(promise => promise.catch(catchHandler))
.then(results => console.log(results))
.catch(() => console.log('Promise.all failed'))
< [ 'Resolved!', { payload: Promise, resolved: false } ]
El problema con esto es que los valores capturados tendrán una interfaz diferente a los valores no capturados, por lo que para limpiar esto puede hacer algo como:
const successHandler = result => ({ payload: result, resolved: true });
Entonces ahora puedes hacer esto:
> Promise.all([a, b].map(result => result.then(successHandler).catch(catchHandler))
.then(results => console.log(results.filter(result => result.resolved))
.catch(() => console.log('Promise.all failed'))
< [ 'Resolved!' ]
Luego, para mantenerlo SECO, obtienes la respuesta de Benjamin:
const reflect = promise => promise
.then(successHandler)
.catch(catchHander)
donde ahora parece
> Promise.all([a, b].map(result => result.then(successHandler).catch(catchHandler))
.then(results => console.log(results.filter(result => result.resolved))
.catch(() => console.log('Promise.all failed'))
< [ 'Resolved!' ]
Los beneficios de la segunda solución son su abstracción y SECO. La desventaja es que tiene más código y debe recordar reflejar todas sus promesas para hacer que las cosas sean consistentes.
Caracterizaría mi solución como explícita y KISS, pero de hecho menos robusta. La interfaz no garantiza que sepa exactamente si la promesa tuvo éxito o no.
Por ejemplo, puede tener esto:
const a = Promise.resolve(new Error('Not beaking, just bad'));
const b = Promise.reject(new Error('This actually didnt work'));
Esto no será atrapado a.catch
, así que
> Promise.all([a, b].map(promise => promise.catch(e => e))
.then(results => console.log(results))
< [ Error, Error ]
No hay forma de saber cuál fue fatal y cuál no. Si eso es importante, entonces querrá aplicar una interfaz que rastree si fue exitosa o no (lo que reflect
sí ocurre).
Si solo desea manejar los errores con gracia, puede tratar los errores como valores indefinidos:
> Promise.all([a.catch(() => undefined), b.catch(() => undefined)])
.then((results) => console.log('Known values: ', results.filter(x => typeof x !== 'undefined')))
< [ 'Resolved!' ]
En mi caso, no necesito saber el error o cómo falló, solo me importa si tengo el valor o no. Dejaré que la función que genera la promesa se preocupe por registrar el error específico.
const apiMethod = () => fetch()
.catch(error => {
console.log(error.message);
throw error;
});
De esa manera, el resto de la aplicación puede ignorar su error si lo desea, y tratarlo como un valor indefinido si lo desea.
Quiero que mis funciones de alto nivel fallen con seguridad y no me preocupe por los detalles de por qué fallaron sus dependencias, y también prefiero KISS a DRY cuando tengo que hacer ese intercambio, que es en última instancia por lo que opté por no usarlo reflect
.