web-dev-qa-db-fra.com

Attendez que toutes les promesses de l'ES6 soient terminées, voire rejetées

Disons que j'ai un ensemble de promesses qui font des demandes de réseau, dont l'une va échouer:

// 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   

Disons que je veux attendre que tout cela soit terminé, qu’il s’agisse d’un échec ou non. Il peut y avoir une erreur de réseau pour une ressource sans laquelle je peux vivre mais que si je peux obtenir, je le souhaite avant de continuer. Je veux gérer les pannes de réseau avec élégance.

Puisque Promises.all ne laisse pas de place à cela, quel est le modèle recommandé pour le gérer sans utiliser une bibliothèque de promesses?

280
Nathan Hagen

La réponse de Benjamin offre une grande abstraction pour résoudre ce problème, mais j'espérais une solution moins abstraite. Le moyen explicite de résoudre ce problème consiste simplement à appeler .catch sur les promesses internes et à renvoyer l'erreur à partir de leur rappel.

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));

Pour aller plus loin, vous pouvez écrire un gestionnaire de capture générique qui ressemble à ceci:

const catchHandler = error => ({ payload: error, resolved: false });

alors vous pouvez faire

> 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 } ]

Le problème avec ceci est que les valeurs capturées auront une interface différente de celle des valeurs non capturées, donc pour nettoyer cela, vous pouvez faire quelque chose comme:

const successHandler = result => ({ payload: result, resolved: true });

Alors maintenant, vous pouvez faire ceci:

> 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!' ]

Alors pour le garder au sec, vous arriverez à la réponse de Benjamin:

const reflect = promise => promise
  .then(successHandler)
  .catch(catchHander)

où il ressemble maintenant

> 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!' ]

La seconde solution présente les avantages suivants: abstention et séchage. L'inconvénient est que vous avez plus de code, et vous devez vous souvenir de tenir compte de toutes vos promesses de cohérence.

Je qualifierais ma solution d’explicite et de KISS, mais en réalité moins robuste. L'interface ne garantit pas que vous savez exactement si la promesse a réussi ou a échoué.

Par exemple, vous pourriez avoir ceci:

const a = Promise.resolve(new Error('Not beaking, just bad'));
const b = Promise.reject(new Error('This actually didnt work'));

Ceci ne sera pas attrapé par a.catch, donc

> Promise.all([a, b].map(promise => promise.catch(e => e))
    .then(results => console.log(results))
< [ Error, Error ]

Il n'y a aucun moyen de savoir lequel était fatal et lequel ne l'était pas. Si cela est important, vous souhaiterez appliquer une interface permettant de déterminer si l'opération a abouti ou non (ce que reflect fait).

Si vous voulez simplement gérer les erreurs avec élégance, vous pouvez simplement les traiter comme des valeurs indéfinies:

> Promise.all([a.catch(() => undefined), b.catch(() => undefined)])
    .then((results) => console.log('Known values: ', results.filter(x => typeof x !== 'undefined')))
< [ 'Resolved!' ]

Dans mon cas, je n'ai pas besoin de connaître l'erreur ou son erreur - je me soucie simplement de savoir si j'ai la valeur ou non. Je laisserai la fonction qui génère la promesse se soucier de la consignation de l'erreur spécifique.

const apiMethod = () => fetch()
  .catch(error => {
    console.log(error.message);
    throw error;
  });

De cette façon, le reste de l'application peut ignorer son erreur s'il le souhaite et le traiter comme une valeur indéfinie s'il le souhaite.

Je veux que mes fonctions de haut niveau échouent en toute sécurité et ne m'inquiètent pas des raisons pour lesquelles ses dépendances ont échoué, et je préfère également KISS à DRY lorsque je dois faire ce compromis - ce qui est finalement a choisi de ne pas utiliser reflect.

50
Nathan Hagen

Bien sûr, vous avez juste besoin d'une reflect:

const reflect = p => p.then(v => ({v, status: "fulfilled" }),
                            e => ({e, status: "rejected" }));

reflect(promise).then((v => {
    console.log(v.status);
});

Ou avec ES5:

function reflect(promise){
    return promise.then(function(v){ return {v:v, status: "resolved" }},
                        function(e){ return {e:e, status: "rejected" }});
}


reflect(promise).then(function(v){
    console.log(v.status);
});

Ou dans votre exemple:

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 === "resolved");
});
228
Benjamin Gruenbaum

Réponse similaire, mais plus idiomatique pour ES6 peut-être:

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>

En fonction du type de valeurs renvoyées, les erreurs peuvent souvent être facilement distinguées (par exemple, utilisez undefined pour "ne vous inquiétez pas", typeof pour des valeurs simples, result.message, result.toString().startsWith("Error:") etc.)

187
jib

J'aime beaucoup la réponse de Benjamin et la façon dont il transforme toutes ses promesses en des résolutions toujours résolues mais parfois avec des erreurs. :)
Voici ma tentative à votre demande juste au cas où vous cherchiez des alternatives. Cette méthode traite simplement les erreurs comme des résultats valides et est codée de la même manière que Promise.all sinon:

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);
  });
}
9
Kuba Wyrostek
var err;
Promise.all([
    promiseOne().catch(function(error) { err = error;}),
    promiseTwo().catch(function(error) { err = error;})
]).then(function() {
    if (err) {
        throw err;
    }
});

Le Promise.all avalera toute promesse rejetée et enregistrera l'erreur dans une variable. Il reviendra dès que toutes les promesses auront été résolues. Ensuite, vous pouvez relancer l'erreur ou faire ce que vous voulez. De cette façon, je suppose que vous obtiendrez le dernier rejet au lieu du premier.

4
martin770

Il y a une proposition pour une fonction qui peut accomplir cela nativement, en Javascript Vanilla: Promise.allSettled. Il en est actuellement à l'étape 3 et il est très probable que ce soit dans les spécifications officielles. C'est très similaire à la fonction reflect dans cette autre réponse . Voici un exemple de la page de proposition. Avant, vous auriez dû faire:

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');

En utilisant Promise.allSettled à la place, ce qui précède sera équivalent à:

const promises = [ fetch('index.html'), fetch('https://does-not-exist/') ];
const results = await Promise.allSettled(promises);
const successfulPromises = results.filter(p => p.status === 'fulfilled');

Une fois que cela fait partie de la spécification et que les navigateurs l'implémentent, vous pourrez l'utiliser sur les navigateurs modernes sans aucune bibliothèque .

4

J'ai eu le même problème et l'ai résolu de la manière suivante:

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(......)

Dans ce cas, Promise.all attendra que chaque promesse vienne dans l'état resolved ou rejected.

Et ayant cette solution, nous "arrêtons l'exécution de catch" d'une manière non bloquante. En fait, nous n'arrêtons rien, nous renvoyons simplement la Promise dans un état en attente qui renvoie une autre Promise lorsqu'elle est résolue après le délai.

4
user1016265

La réponse de Benjamin Gruenbaum est bien sûr géniale. Mais je peux aussi voir si le point de vue de Nathan Hagen avec le niveau d'abstraction semble vague. Avoir des propriétés d'objet courtes comme e & v n'aide pas non plus, mais cela pourrait bien sûr être changé.

En Javascript, il existe un objet d'erreur standard, appelé Error ,. Idéalement, vous lancez toujours une instance/un descendant de ceci. L'avantage est que vous pouvez faire instanceof Error, et vous savez que quelque chose est une erreur.

Donc, en utilisant cette idée, voici mon point de vue sur le problème.

En gros, attrapez l’erreur, si l’erreur n’est pas de type Error, placez-la dans un objet Error. Le tableau résultant aura soit des valeurs résolues, soit des objets d'erreur que vous pouvez vérifier.

L'instance de l'intérieur du crochet est au cas où vous utiliseriez une bibliothèque externe qui aurait peut-être reject("error"), au lieu de reject(new Error("error")).

Bien sûr, vous pourriez avoir des promesses si vous résolviez une erreur, mais dans ce cas, il serait probablement logique de traiter comme une erreur de toute façon, comme le montre le dernier exemple.

Un autre avantage de cela, la destruction de tableaux est simplifiée.

const [value1, value2] = PromiseAllCatch(promises);
if (!(value1 instanceof Error)) console.log(value1);

Au lieu de

const [{v: value1, e: error1}, {v: value2, e: error2}] = Promise.all(reflect..
if (!error1) { console.log(value1); }

Vous pourriez faire valoir que la vérification !error1 est plus simple qu'une instance de, mais vous devez également détruire les deux v & e.

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();
2
Keith

Cela devrait être compatible avec comment Q le fait-il :

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,
        }))));
    };
}
1
mpen

Je pense que ce qui suit offre une approche légèrement différente ... comparez fn_fast_fail() avec fn_slow_fail()... bien que le dernier n’échoue pas en tant que tel ... vous pouvez vérifier si l’un ou les deux de a et b est une instance de Error et throwError si vous voulez qu'il atteigne le bloc catch (par exemple if (b instanceof Error) { throw b; }). Voir le jsfiddle .

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
0
drmrbrewer

Voici ma settledPromiseAll() personnalisé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;
  });
};

Comparé à Promise.all

  • Si toutes les promesses sont résolues, il fonctionne exactement comme le standard.

  • Si une ou plusieurs promesses sont rejetées, le premier renvoyé est renvoyé de la même manière que le mandat standard, mais contrairement à ce qu’il attend, il attend que toutes les promesses soient résolues/rejetées.

Pour les plus courageux, nous pourrions changer Promise.all():

(function() {
  var stdAll = Promise.all;

  Promise.all = function(values, wait) {
    if(!wait)
      return stdAll.call(Promise, values);

    return settledPromiseAll(values);
  }
})();

PRUDENT. En général, nous ne modifions jamais les fonctions intégrées, car cela pourrait endommager d'autres bibliothèques JS non liées ou entrer en conflit avec les modifications futures des normes JS.

Ma settledPromiseall est rétro-compatible avec Promise.all et étend ses fonctionnalités.

Les personnes qui élaborent des normes - pourquoi ne pas inclure ceci dans une nouvelle norme Promise? 

0
Edward

Vous pouvez exécuter votre logique séquentiellement via l'exécuteur synchrone nsynjs . Il s'interrompt sur chaque promesse, attend la résolution/le rejet et affecte le résultat de la résolution à la propriété data ou génère une exception (pour le traitement, vous aurez besoin du bloc try/catch). Voici un exemple:

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');
});
<script src="https://rawgit.com/amaksr/nsynjs/master/nsynjs.js"></script>

0
amaksr

Je sais que cette question a beaucoup de réponses, et je suis sûr que vous devez (si ce ne sont pas toutes) être correctes ..__ Cependant, il m'a été très difficile de comprendre la logique/le flux de ces réponses.

J'ai donc regardé l'implémentation d'origine sur Promise.all() et j'ai essayé d'imiter cette logique - à l'exception de ne pas arrêter l'exécution si une promesse échouait. 

public promiseExecuteAll(promisesList: Promise<any>[]): Promise<{ data: any, isSuccess: boolean }[]>
{
  const result: { data: any, isSuccess: boolean }[] = [];
  let count: number = 0;

  const 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); }
        });
    });
  });

  return promise;
}

Explication: 
- Boucle sur l'entrée promisesList et exécute chaque promesse.
- Peu importe si la promesse est résolue ou rejetée: enregistrez le résultat de la promesse dans un tableau result conformément à index. Enregistrez également le statut de résolution/rejet (isSuccess).
- Une fois toutes les promesses complétées, renvoyez une promesse avec le résultat de toutes les autres.

Exemple d'utilisation:

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
*/
0
Gil Epshtain