web-dev-qa-db-ger.com

Warten Sie, bis alle ES6-Versprechen vollständig sind, sogar abgelehnte Versprechen

Nehmen wir an, ich habe eine Reihe von Versprechen, die Netzwerkanfragen stellen, von denen eine versagt:

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

Sagen wir, ich möchte warten, bis alle fertig sind, unabhängig davon, ob einer versagt hat. Es könnte einen Netzwerkfehler für eine Ressource geben, ohne die ich leben kann, aber wenn ich sie bekomme, möchte ich, bevor ich fortfahre. Ich möchte vernünftig mit Netzwerkfehlern umgehen.

Da Promises.all dafür keinen Platz lässt, wie wird das empfohlen, um dies zu tun, ohne eine Versprechenbibliothek zu verwenden?

280
Nathan Hagen

Benjamins Antwort bietet eine großartige Abstraktion für die Lösung dieses Problems, aber ich hoffte auf eine weniger abstrahierte Lösung. Um dieses Problem zu beheben, rufen Sie einfach .catch für die internen Zusagen auf und geben den Fehler aus dem Rückruf zurück.

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

Wenn Sie diesen Schritt noch weiter gehen, könnten Sie einen generischen catch-Handler schreiben, der folgendermaßen aussieht:

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

dann kannst du tun

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

Das Problem dabei ist, dass die erfassten Werte eine andere Schnittstelle haben als die nicht erfassten Werte. Um dies zu bereinigen, können Sie Folgendes tun:

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

Jetzt können Sie das tun:

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

Dann, um es trocken zu halten, kommen Sie zu Benjamins Antwort:

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

wo es jetzt aussieht

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

Die Vorteile der zweiten Lösung sind, dass sie abstrahiert und DRY sind. Der Nachteil ist, dass Sie mehr Code haben, und Sie müssen daran denken, all Ihre Versprechen zu reflektieren, um die Dinge konsistent zu machen.

Ich würde meine Lösung als explizit und KISS bezeichnen, aber in der Tat weniger robust. Die Benutzeroberfläche garantiert nicht, dass Sie genau wissen, ob das Versprechen erfolgreich war oder nicht.

Zum Beispiel könnten Sie folgendes haben:

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

Dies wird nicht von a.catch erwischt, also

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

Es gibt keine Möglichkeit herauszufinden, welcher tödlich war und welcher nicht. Wenn dies wichtig ist, sollten Sie eine Durchsetzung erzwingen und ein Interface erstellen, das verfolgt, ob es erfolgreich war oder nicht (was reflect tut).

Wenn Sie nur Fehler ordnungsgemäß behandeln möchten, können Sie Fehler einfach als undefinierte Werte behandeln:

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

In meinem Fall muss ich den Fehler nicht wissen oder wissen, wie er fehlgeschlagen ist - es ist mir nur wichtig, ob ich den Wert habe oder nicht. Ich lasse die Funktion, die das Versprechen generiert, sich um die Protokollierung des spezifischen Fehlers kümmern.

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

Auf diese Weise kann der Rest der Anwendung den Fehler ignorieren, wenn er möchte, und ihn als undefinierten Wert behandeln, wenn er möchte.

Ich möchte, dass meine High-Level-Funktionen sicher ausfallen und sich keine Sorgen darüber machen, warum ihre Abhängigkeiten fehlgeschlagen sind, und ich bevorzuge KISS to DRY, wenn ich diese Kompromisse machen muss - welche Deshalb habe ich mich entschieden, reflect nicht zu verwenden.

50
Nathan Hagen

Klar, du brauchst nur eine reflect:

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

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

Oder mit 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);
});

Oder in deinem Beispiel:

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

Ähnliche Antwort, aber idiotischer für ES6:

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>

Abhängig von der Art der zurückgegebenen Werte können Fehler oft leicht genug unterschieden werden (z. B. verwenden Sie undefined für "egal", typeof für einfache Nichtobjektwerte, result.message, result.toString().startsWith("Error:") usw.).

187
jib

Ich mag Benjamins Antwort wirklich und wie er alle Versprechen im Grunde in immer auflösende, aber manchmal auch mit Fehler als Ergebnis umwandelt. :)
Hier ist mein Versuch auf Ihre Anfrage, falls Sie nach Alternativen suchen. Diese Methode behandelt Fehler einfach als gültige Ergebnisse und ist ähnlich wie Promise.all codiert, ansonsten:

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

Der Promise.all schluckt jedes abgelehnte Versprechen und speichert den Fehler in einer Variablen. Er wird zurückgegeben, wenn alle Versprechen gelöst wurden. Dann können Sie den Fehler erneut auswerfen oder tun, was auch immer. Auf diese Weise würden Sie wahrscheinlich die letzte Ablehnung anstelle der ersten ablehnen.

4
martin770

In Vanilla Javascript gibt es einen Vorschlag für eine Funktion, die dies nativ ausführen kann: Promise.allSettled. Es befindet sich derzeit in Phase 3 und wird sehr wahrscheinlich in die offizielle Spezifikation aufgenommen. Es ist der Funktion reflect in dieser anderen Antwort sehr ähnlich. Hier ist ein Beispiel von der Vorschlagsseite. Vorher hätten Sie Folgendes tun müssen:

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

Wenn Sie stattdessen Promise.allSettled verwenden, entspricht dies:

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

Sobald dies Teil der Spezifikation ist und die Browser es implementieren, können Sie es auf modernen Browsern verwenden ohne Bibliotheken .

4

Ich hatte das gleiche Problem und habe es auf folgende Weise gelöst:

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

In diesem Fall wartet Promise.all, bis jedes Promise in den Status resolved oder rejected gerät.

Und mit dieser Lösung "stoppen wir die Ausführung von catch" nicht blockierend. Tatsächlich stoppen wir nichts, wir geben nur die Promise in einen ausstehenden Zustand zurück, der eine andere Promise zurückgibt, wenn sie nach dem Timeout aufgelöst wird.

4
user1016265

Benjamin Grünbaums Antwort ist natürlich toll. Aber ich kann auch sehen, wo Nathan Hagen Sichtweise mit der Abstraktionsebene vage erscheint. Es hilft auch nicht, kurze Objekteigenschaften wie e & v zu haben, aber das könnte natürlich geändert werden.

In Javascript gibt es das Standard-Fehlerobjekt Error. Idealerweise wirfst du immer eine Instanz/einen Nachkommen davon. Der Vorteil ist, dass Sie instanceof Error ausführen können und wissen, dass etwas ein Fehler ist.

Wenn ich diese Idee verwende, ist hier meine Ansicht zum Problem.

Grundsätzlich kann der Fehler behoben werden. Wenn der Fehler nicht vom Typ Fehler ist, binden Sie den Fehler in ein Fehlerobjekt ein. Das resultierende Array enthält entweder aufgelöste Werte oder Fehlerobjekte, die Sie überprüfen können.

Die Instanz innerhalb des Catch ist für den Fall, dass Sie eine externe Bibliothek verwenden, die möglicherweise reject("error") anstelle von reject(new Error("error")) ausgeführt hat.

Natürlich könnten Sie Versprechungen haben, wenn Sie einen Fehler beheben würden, aber in diesem Fall wäre es höchstwahrscheinlich sinnvoll, ihn trotzdem als Fehler zu behandeln, wie das letzte Beispiel zeigt.

Ein weiterer Vorteil dabei ist, dass die Zerstörung von Arrays einfach gehalten wird.

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

Anstatt

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

Sie könnten argumentieren, dass die !error1 Prüfung einfacher ist als eine Instanz von, aber Sie müssen auch beide v & e zerstören.

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

Dies sollte im Einklang stehen mit wie Q es tut :

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

Ich denke, das Folgende bietet einen etwas anderen Ansatz ... vergleiche fn_fast_fail() mit fn_slow_fail()... obwohl letzteres nicht als solches ausfällt ... Sie können prüfen, ob a und b eine Instanz von Error und throw sind Error wenn Sie möchten, dass der catch-Block erreicht wird (z. B. if (b instanceof Error) { throw b; }). Siehe 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

Hier ist meine Gewohnheit settledPromiseAll() 

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

Im Vergleich zu Promise.all

  • Wenn alle Versprechen gelöst werden, funktioniert sie genau wie das Standardversprechen.

  • Wenn eines oder mehrere Zusagen abgelehnt werden, wird das erste abgelehnt, was dem Standardversprechen entspricht. Anders als beim Versprechen wird abgewartet, bis alle Versprechen aufgelöst/abgelehnt werden.

Für die Mutigen könnten wir Promise.all() ändern:

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

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

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

VORSICHTIG. Im Allgemeinen ändern wir niemals eingebaute Ins, da dies andere nicht zusammenhängende JS-Bibliotheken zerstören oder mit zukünftigen Änderungen der JS-Standards kollidieren könnte.

Meine settledPromiseall ist abwärtskompatibel mit Promise.all und erweitert ihre Funktionalität.

Menschen, die Standards entwickeln - warum dies nicht in einen neuen Promise-Standard aufnehmen? 

0
Edward

Sie können Ihre Logik über den synchronen Executor nsynjs sequentiell ausführen. Es wird bei jedem Versprechen angehalten, auf Auflösung/Ablehnung warten und entweder das Ergebnis von resolly der data -Eigenschaft zuweisen oder eine Ausnahme auslösen (für die Behandlung, dass Sie einen try/catch-Block benötigen). Hier ist ein Beispiel:

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

Ich weiß, dass diese Frage viele Antworten hat, und ich bin mir sicher, dass sie (wenn nicht alle) richtig sind.

Also habe ich mir die Original-Implementierung von Promise.all() angesehen und versucht, diese Logik nachzuahmen - mit der Ausnahme, dass die Ausführung nicht angehalten wird, wenn eine Promise fehlgeschlagen ist. 

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

Erläuterung: 
- Schleife über die Eingabe promisesList und jedes Promise ausführen.
- Unabhängig davon, ob das Versprechen gelöst oder abgelehnt wurde: Speichern Sie das Ergebnis des Versprechens in einem result-Array gemäß der index. Speichern Sie auch den Auflösungs-/Ablehnungsstatus (isSuccess).
- Wenn alle Versprechen abgeschlossen sind, geben Sie ein Versprechen mit dem Ergebnis aller anderen zurück.

Anwendungsbeispiel:

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