javascript - promises - Espere hasta que se completen todas las promesas de ES6, incluso las promesas rechazadas
promise typescript (16)
Digamos que tengo un conjunto de promesas que están haciendo solicitudes de red, de las cuales una fallará:
// http://does-not-exist will throw a TypeError
var arr = [ fetch(''index.html''), fetch(''http://does-not-exist'') ]
Promise.all(arr)
.then(res => console.log(''success'', res))
.catch(err => console.log(''error'', err)) // This is executed
Digamos que quiero esperar hasta que todo esto haya terminado, independientemente de si uno ha fallado. Puede haber un error de red para un recurso sin el cual puedo vivir, pero que si puedo obtener, quiero antes de continuar. Quiero manejar fallas de red con gracia.
Dado que
Promises.all
no deja espacio para esto, ¿cuál es el patrón recomendado para manejar esto, sin usar una biblioteca de promesas?
Aquí está mi costumbre
settledPromiseAll()
var err = [fetch(''index.html'').then((success) => { return Promise.resolve(success); }).catch((e) => { return Promise.resolve(e); }),
fetch(''http://does-not-exist'').then((success) => { return Promise.resolve(success); }).catch((e) => { return Promise.resolve(e); })];
Promise.all(err)
.then(function (res) { console.log(''success'', res) })
.catch(function (err) { console.log(''error'', err) }) //never executed
En comparación con
Promise.all
-
Si se resuelven todas las promesas, funciona exactamente como la estándar.
-
Si se rechaza una o más promesas, devuelve la primera rechazada de manera muy similar a la estándar pero, a diferencia de esto, espera que todas las promesas se resuelvan / rechacen.
Para los valientes podríamos cambiar
Promise.all()
:
function synchronousCode() {
function myFetch(url) {
try {
return window.fetch(url).data;
}
catch (e) {
return {status: ''failed:''+e};
};
};
var arr=[
myFetch("https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"),
myFetch("https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/NONEXISTANT.js"),
myFetch("https://ajax.NONEXISTANT123.com/ajax/libs/jquery/2.0.0/NONEXISTANT.js")
];
console.log(''array is ready:'',arr[0].status,arr[1].status,arr[2].status);
};
nsynjs.run(synchronousCode,{},function(){
console.log(''done'');
});
CUIDADO En general, nunca cambiamos los elementos integrados, ya que podría romper otras bibliotecas JS no relacionadas o chocar con futuros cambios en los estándares JS.
Mi establecido
settledPromiseall
es compatible con versiones anteriores de
Promise.all
y extiende su funcionalidad.
Personas que están desarrollando estándares: ¿por qué no incluir esto en un nuevo estándar de Promise?
Claro, solo necesitas un
reflect
:
const reflect = p => p.then(v => ({v, status: "fulfilled" }),
e => ({e, status: "rejected" }));
reflect(promise).then((v => {
console.log(v.status);
});
O con ES5:
function reflect(promise){
return promise.then(function(v){ return {v:v, status: "fulfilled" }},
function(e){ return {e:e, status: "rejected" }});
}
reflect(promise).then(function(v){
console.log(v.status);
});
O en tu ejemplo:
var arr = [ fetch(''index.html''), fetch(''http://does-not-exist'') ]
Promise.all(arr.map(reflect)).then(function(results){
var success = results.filter(x => x.status === "fulfilled");
});
Creo que lo siguiente ofrece un enfoque ligeramente diferente ... compare
fn_fast_fail()
con
fn_slow_fail()
... aunque este último no falla como tal ... puede verificar si uno o ambos de
b
es una instancia de
Error
y
throw
ese
Error
si desea que llegue al bloque
catch
(por ejemplo,
if (b instanceof Error) { throw b; }
).
Ver el
jsfiddle
.
(function() {
var stdAll = Promise.all;
Promise.all = function(values, wait) {
if(!wait)
return stdAll.call(Promise, values);
return settledPromiseAll(values);
}
})();
Esto debería ser coherente con cómo lo hace Q :
const [{v: value1, e: error1}, {v: value2, e: error2}] = Promise.all(reflect..
if (!error1) { console.log(value1); }
Hay una
proposal
para una función que puede lograr esto de forma nativa, en Javascript vainilla:
Promise.allSettled
.
Actualmente se encuentra en la etapa 4, y es muy probable que llegue a la especificación oficial.
Es muy similar a la función de
reflect
en
esta otra respuesta
.
Aquí hay un ejemplo, de la página de propuesta.
Antes, habrías tenido que hacer:
function reflect(promise) {
return promise.then(
(v) => {
return { status: ''fulfilled'', value: v };
},
(error) => {
return { status: ''rejected'', reason: error };
}
);
}
const promises = [ fetch(''index.html''), fetch(''https://does-not-exist/'') ];
const results = await Promise.all(promises.map(reflect));
const successfulPromises = results.filter(p => p.status === ''fulfilled'');
Usando
Promise.allSettled
en
Promise.allSettled
lugar, lo anterior será equivalente a:
const promises = [ fetch(''index.html''), fetch(''https://does-not-exist/'') ];
const results = await Promise.allSettled(promises);
const successfulPromises = results.filter(p => p.status === ''fulfilled'');
Una vez que esto se convierta en parte de la especificación y los navegadores lo implementen, podrá usarlo en navegadores modernos sin ninguna biblioteca .
Se ha enviado en Chrome 76, por lo que en él, el siguiente fragmento debe ejecutarse sin problemas:
var err;
Promise.all([
promiseOne().catch(function(error) { err = error;}),
promiseTwo().catch(function(error) { err = error;})
]).then(function() {
if (err) {
throw err;
}
});
Salida:
const fetch = (url) => {
return node-fetch(url)
.then(result => result.json())
.catch((e) => {
return new Promise((resolve) => setTimeout(() => resolve(fetch(url)), timeout));
});
};
tasks = [fetch(url1), fetch(url2) ....];
Promise.all(tasks).then(......)
Mientras tanto, aquí hay un polyfill que cumple con las especificaciones.
He estado usando los siguientes códigos desde ES5.
const p1 = Promise.resolve("OK");
const p2 = Promise.reject(new Error(":-("));
const p3 = Promise.resolve(1000);
promiseExecuteAll([p1, p2, p3]).then((data) => {
data.forEach(value => console.log(`${ value.isSuccess ? ''Resolve'' : ''Reject'' } >> ${ value.data }`));
});
/* Output:
Resolve >> OK
Reject >> :-(
Resolve >> 1000
*/
La firma de uso es como
Promise.all
.
La principal diferencia es que
Promise.wait
esperará a que todas las promesas terminen sus trabajos.
La respuesta de Benjamin Gruenbaum es, por supuesto, genial.
Pero también puedo ver si el punto de vista de Nathan Hagen con el nivel de abstracción parece vago.
Tener propiedades de objetos cortos como
e & v
tampoco ayuda, pero por supuesto eso podría cambiarse.
En Javascript hay un objeto de error estándar, llamado
Error
Idealmente, siempre arrojas una instancia / descendiente de esto.
La ventaja es que puede hacer una
instanceof Error
, y sabe que algo es un error.
Entonces, usando esta idea, aquí está mi opinión sobre el problema.
Básicamente, detecte el error, si el error no es del tipo Error, envuelva el error dentro de un objeto Error. La matriz resultante tendrá valores resueltos u objetos de error que puede verificar.
La instancia de dentro de la captura es en caso de que use alguna biblioteca externa que tal vez
reject("error")
, en lugar de
reject(new Error("error"))
.
Por supuesto, podría tener promesas si resuelve un error, pero en ese caso lo más probable es que tenga sentido tratarlo como un error, como muestra el último ejemplo.
Otra ventaja de hacerlo es que la destrucción de la matriz se mantiene simple.
function PromiseAllCatch(promises) {
return Promise.all(promises.map(async m => {
try {
return await m;
} catch(e) {
if (e instanceof Error) return e;
return new Error(e);
}
}));
}
async function test() {
const ret = await PromiseAllCatch([
(async () => "this is fine")(),
(async () => {throw new Error("oops")})(),
(async () => "this is ok")(),
(async () => {throw "Still an error";})(),
(async () => new Error("resolved Error"))(),
]);
console.log(ret);
console.log(ret.map(r =>
r instanceof Error ? "error" : "ok"
).join(" : "));
}
test();
En vez de
var p1 = new Promise((resolve, reject) => {
setTimeout(() => resolve(''p1_delayed_resolvement''), 2000);
});
var p2 = new Promise((resolve, reject) => {
reject(new Error(''p2_immediate_rejection''));
});
var fn_fast_fail = async function () {
try {
var [a, b] = await Promise.all([p1, p2]);
console.log(a); // "p1_delayed_resolvement"
console.log(b); // "Error: p2_immediate_rejection"
} catch (err) {
console.log(''ERROR:'', err);
}
}
var fn_slow_fail = async function () {
try {
var [a, b] = await Promise.all([
p1.catch(error => { return error }),
p2.catch(error => { return error })
]);
console.log(a); // "p1_delayed_resolvement"
console.log(b); // "Error: p2_immediate_rejection"
} catch (err) {
// we don''t reach here unless you throw the error from the `try` block
console.log(''ERROR:'', err);
}
}
fn_fast_fail(); // fails immediately
fn_slow_fail(); // waits for delayed promise to resolve
Podría argumentar que la verificación
!error1
es más simple que una instancia de, pero también tiene que destruir ambos
v & e
.
const settledPromiseAll = function(promisesArray) {
var savedError;
const saveFirstError = function(error) {
if (!savedError) savedError = error;
};
const handleErrors = function(value) {
return Promise.resolve(value).catch(saveFirstError);
};
const allSettled = Promise.all(promisesArray.map(handleErrors));
return allSettled.then(function(resolvedPromises) {
if (savedError) throw savedError;
return resolvedPromises;
});
};
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 llamar a
.catch
en 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 quedará atrapado por una
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 haga un seguimiento de si tuvo éxito o no (lo que
reflect
hace).
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 esa compensación, que es la razón por la que opté por no usar
reflect
.
No sé qué biblioteca de promesas está utilizando, pero la mayoría tiene algo como allSettled .
Editar: Ok, ya que desea usar ES6 simple sin bibliotecas externas, no existe tal método.
En otras palabras: debe realizar un bucle de sus promesas manualmente y resolver una nueva promesa combinada tan pronto como se resuelvan todas las promesas.
Puede ejecutar su lógica secuencialmente a través del ejecutor síncrono
nsynjs
.
Pausará cada promesa, esperará la resolución / rechazo y asignará el resultado de la resolución a
data
propiedad de
data
, o lanzará una excepción (para manejar que necesitará el bloque try / catch).
Aquí hay un ejemplo:
Promise.wait = function(promiseQueue){
if( !Array.isArray(promiseQueue) ){
return Promise.reject(''Given parameter is not an array!'');
}
if( promiseQueue.length === 0 ){
return Promise.resolve([]);
}
return new Promise((resolve, reject) =>{
let _pQueue=[], _rQueue=[], _readyCount=false;
promiseQueue.forEach((_promise, idx) =>{
// Create a status info object
_rQueue.push({rejected:false, seq:idx, result:null});
_pQueue.push(Promise.resolve(_promise));
});
_pQueue.forEach((_promise, idx)=>{
let item = _rQueue[idx];
_promise.then(
(result)=>{
item.resolved = true;
item.result = result;
},
(error)=>{
item.resolved = false;
item.result = error;
}
).then(()=>{
_readyCount++;
if ( _rQueue.length === _readyCount ) {
let result = true;
_rQueue.forEach((item)=>{result=result&&item.resolved;});
(result?resolve:reject)(_rQueue);
}
});
});
});
};
public promiseExecuteAll(promisesList: Promise<any>[] = []): Promise<{ data: any, isSuccess: boolean }[]>
{
let promise: Promise<{ data: any, isSuccess: boolean }[]>;
if (promisesList.length)
{
const result: { data: any, isSuccess: boolean }[] = [];
let count: number = 0;
promise = new Promise<{ data: any, isSuccess: boolean }[]>((resolve, reject) =>
{
promisesList.forEach((currentPromise: Promise<any>, index: number) =>
{
currentPromise.then(
(data) => // Success
{
result[index] = { data, isSuccess: true };
if (promisesList.length <= ++count) { resolve(result); }
},
(data) => // Error
{
result[index] = { data, isSuccess: false };
if (promisesList.length <= ++count) { resolve(result); }
});
});
});
}
else
{
promise = Promise.resolve([]);
}
return promise;
}
Realmente me gusta la respuesta de Benjamin, y cómo él básicamente convierte todas las promesas en siempre resueltas, pero a veces con errores como resultado.
:)
Aquí está mi intento de su solicitud en caso de que estuviera buscando alternativas.
Este método simplemente trata los errores como resultados válidos y se codifica de manera similar a
Promise.all
.
Promise.settle = function(promises) {
var results = [];
var done = promises.length;
return new Promise(function(resolve) {
function tryResolve(i, v) {
results[i] = v;
done = done - 1;
if (done == 0)
resolve(results);
}
for (var i=0; i<promises.length; i++)
promises[i].then(tryResolve.bind(null, i), tryResolve.bind(null, i));
if (done == 0)
resolve(results);
});
}
Respuesta similar, pero más idiomática para ES6 quizás:
const a = Promise.resolve(1);
const b = Promise.reject(new Error(2));
const c = Promise.resolve(3);
Promise.all([a, b, c].map(p => p.catch(e => e)))
.then(results => console.log(results)) // 1,Error: 2,3
.catch(e => console.log(e));
const console = { log: msg => div.innerHTML += msg + "<br>"};
<div id="div"></div>
Dependiendo del tipo (s) de valores devueltos, los errores a menudo se pueden distinguir con bastante facilidad (por ejemplo, use
undefined
para "no me importa",
typeof
para valores simples que no sean objetos,
result.message
,
result.toString().startsWith("Error:")
etc.)
Sé que esta pregunta tiene muchas respuestas, y estoy seguro de que deben (si no todas) son correctas. Sin embargo, fue muy difícil para mí entender la lógica / flujo de estas respuestas.
Así que miré la Implementación original en
Promise.all()
, e intenté imitar esa lógica, con la excepción de no detener la ejecución si fallaba una Promesa.
public promiseExecuteAll(promisesList: Promise<any>[] = []): Promise<{ data: any, isSuccess: boolean }[]> { let promise: Promise<{ data: any, isSuccess: boolean }[]>; if (promisesList.length) { const result: { data: any, isSuccess: boolean }[] = []; let count: number = 0; promise = new Promise<{ data: any, isSuccess: boolean }[]>((resolve, reject) => { promisesList.forEach((currentPromise: Promise<any>, index: number) => { currentPromise.then( (data) => // Success { result[index] = { data, isSuccess: true }; if (promisesList.length <= ++count) { resolve(result); } }, (data) => // Error { result[index] = { data, isSuccess: false }; if (promisesList.length <= ++count) { resolve(result); } }); }); }); } else { promise = Promise.resolve([]); } return promise; }
Explicación:
- Pase por la lista de
promisesList
entrada y ejecute cada promesa.
- No importa si la Promesa se resolvió o rechazó: guarde el resultado de la Promesa en una matriz de
result
acuerdo con el
index
.
Guarde también el estado de resolución / rechazo (
isSuccess
).
- Una vez completadas todas las promesas, devuelve una promesa con el resultado de todas las demás.
Ejemplo de uso:
const p1 = Promise.resolve("OK"); const p2 = Promise.reject(new Error(":-(")); const p3 = Promise.resolve(1000); promiseExecuteAll([p1, p2, p3]).then((data) => { data.forEach(value => console.log(`${ value.isSuccess ? ''Resolve'' : ''Reject'' } >> ${ value.data }`)); }); /* Output: Resolve >> OK Reject >> :-( Resolve >> 1000 */
Tuve el mismo problema y lo resolví de la siguiente manera:
const [value1, value2] = PromiseAllCatch(promises);
if (!(value1 instanceof Error)) console.log(value1);
En ese caso,
Promise.all
esperará a que cada Promesa entre en estado
resolved
o
rejected
.
Y teniendo esta solución, estamos "deteniendo la ejecución de
catch
" de una manera no bloqueante.
De hecho, no detenemos nada, solo devolvemos la
Promise
en un estado pendiente que devuelve otra
Promise
cuando se resuelve después del tiempo de espera.
Yo lo haría:
<script src="https://rawgit.com/amaksr/nsynjs/master/nsynjs.js"></script>
if(!Promise.allSettled) {
Promise.allSettled = function (promises) {
return Promise.all(promises.map(p => Promise.resolve(p).then(v => ({
state: ''fulfilled'',
value: v,
}), r => ({
state: ''rejected'',
reason: r,
}))));
};
}
Promise.all
se tragará cualquier promesa rechazada y almacenará el error en una variable, por lo que volverá cuando se hayan resuelto todas las promesas.
Luego puede volver a tirar el error o hacer lo que sea.
De esta manera, supongo que sacarías el último rechazo en lugar del primero.