Unterbrechen Sie die Versprechen-Kette und rufen Sie eine Funktion auf, die auf dem Schritt in der Kette basiert, in dem sie unterbrochen (abgelehnt) wurde.

135

Aktualisieren:

Um zukünftigen Zuschauern dieses Beitrags zu helfen, habe ich diese Demo von Plumas Antwort erstellt .

Frage:

Mein Ziel scheint ziemlich einfach zu sein.

  step(1)
  .then(function() {
    return step(2);
  }, function() {
    stepError(1);
    return $q.reject();
  })
  .then(function() {

  }, function() {
    stepError(2);
  });

  function step(n) {
    var deferred = $q.defer();
    //fail on step 1
    (n === 1) ? deferred.reject() : deferred.resolve();
    return deferred.promise;
  }
  function stepError(n) {
    console.log(n); 
  }

Das Problem hierbei ist, dass, wenn ich in Schritt 1 versage, beide stepError(1)UND stepError(2)ausgelöst werden. Wenn ich es nicht tun , return $q.rejectdann stepError(2)wird nicht gefeuert werden, aber step(2)wird, was ich verstehe. Ich habe alles erreicht, außer was ich versuche zu tun.

Wie schreibe ich Versprechen, damit ich bei Ablehnung eine Funktion aufrufen kann, ohne alle Funktionen in der Fehlerkette aufzurufen? Oder gibt es einen anderen Weg, dies zu erreichen?

Hier ist eine Live-Demo , mit der Sie arbeiten können.

Aktualisieren:

Ich habe es irgendwie gelöst. Hier fange ich den Fehler am Ende der Kette ab und übergebe die Daten an, reject(data)damit ich weiß, welches Problem in der Fehlerfunktion zu behandeln ist. Dies entspricht eigentlich nicht meinen Anforderungen, da ich nicht von den Daten abhängig sein möchte. Es wäre lahm, aber in meinem Fall wäre es sauberer, einen Fehlerrückruf an die Funktion zu übergeben, anstatt sich auf die zurückgegebenen Daten zu verlassen, um zu bestimmen, was zu tun ist.

Live-Demo hier (klicken).

step(1)
  .then(function() {
    return step(2);
  })
  .then(function() {
    return step(3);
  })
  .then(false, 
    function(x) {
      stepError(x);
    }
  );
  function step(n) {
    console.log('Step '+n);
    var deferred = $q.defer();
    (n === 1) ? deferred.reject(n) : deferred.resolve(n);
    return deferred.promise;
  }
  function stepError(n) {
    console.log('Error '+n); 
  }
m59
quelle
1
Es gibt eine asynchrone Javascript-Bibliothek, die helfen könnte, wenn dies komplizierter wird
Lucuma
Promise.prototype.catch()Beispiele auf MDN zeigen eine Lösung für genau dieselben Probleme.
Toraritte

Antworten:

199

Der Grund, warum Ihr Code nicht wie erwartet funktioniert, ist, dass er tatsächlich etwas anderes tut, als Sie denken.

Angenommen, Sie haben etwa Folgendes:

stepOne()
.then(stepTwo, handleErrorOne)
.then(stepThree, handleErrorTwo)
.then(null, handleErrorThree);

Um besser zu verstehen, was passiert, tun wir so, als wäre dies synchroner Code mit try/ catchblockiert:

try {
    try {
        try {
            var a = stepOne();
        } catch(e1) {
            a = handleErrorOne(e1);
        }
        var b = stepTwo(a);
    } catch(e2) {
        b = handleErrorTwo(e2);
    }
    var c = stepThree(b);
} catch(e3) {
    c = handleErrorThree(e3);
}

Der onRejectedHandler (das zweite Argument von then) ist im Wesentlichen ein Fehlerkorrekturmechanismus (wie ein catchBlock). Wenn ein Fehler eingegeben wird handleErrorOne, wird er vom nächsten catch-Block ( catch(e2)) abgefangen und so weiter.

Dies ist offensichtlich nicht das, was Sie beabsichtigt haben.

Nehmen wir an, wir möchten, dass die gesamte Auflösungskette versagt, egal was schief geht:

stepOne()
.then(function(a) {
    return stepTwo(a).then(null, handleErrorTwo);
}, handleErrorOne)
.then(function(b) {
    return stepThree(b).then(null, handleErrorThree);
});

Hinweis: Wir können das handleErrorOnedort belassen, wo es ist, da es nur aufgerufen wird, wenn stepOnees abgelehnt wird (es ist die erste Funktion in der Kette, sodass wir wissen, dass wenn die Kette zu diesem Zeitpunkt abgelehnt wird, dies nur aufgrund des Versprechens dieser Funktion möglich ist). .

Die wichtige Änderung besteht darin, dass die Fehlerbehandlungsroutinen für die anderen Funktionen nicht Teil der Hauptversprechen-Kette sind. Stattdessen hat jeder Schritt eine eigene "Unterkette" mit einer onRejected, die nur aufgerufen wird, wenn der Schritt abgelehnt wurde (aber von der Hauptkette nicht direkt erreicht werden kann).

Der Grund dafür ist, dass beide onFulfilledund onRejectedoptionale Argumente für die thenMethode sind. Wenn ein Versprechen erfüllt (dh gelöst) wird und der nächste thenin der Kette keinen onFulfilledHandler hat, wird die Kette fortgesetzt, bis es einen mit einem solchen Handler gibt.

Dies bedeutet, dass die folgenden zwei Zeilen äquivalent sind:

stepOne().then(stepTwo, handleErrorOne)
stepOne().then(null, handleErrorOne).then(stepTwo)

Die folgende Zeile entspricht jedoch nicht den beiden oben genannten:

stepOne().then(stepTwo).then(null, handleErrorOne)

$qAngulars Versprechensbibliothek basiert auf der QBibliothek von kriskowal (die eine reichhaltigere API hat, aber alles enthält, was Sie finden können $q). Die API-Dokumente von Q auf GitHub könnten sich als nützlich erweisen. Q implementiert die Promises / A + -Spezifikation , in der detailliert beschrieben wird, wie thenund wie das Verhalten bei der Auflösung von Versprechen genau funktioniert.

BEARBEITEN:

Denken Sie auch daran, dass Sie, wenn Sie in Ihrem Fehlerbehandler aus der Kette ausbrechen möchten, ein abgelehntes Versprechen zurückgeben oder einen Fehler auslösen müssen (der automatisch abgefangen und in ein abgelehntes Versprechen eingewickelt wird). Wenn Sie kein Versprechen zurückgeben, wird thender Rückgabewert in ein Lösungsversprechen für Sie eingeschlossen.

Dies bedeutet, dass Sie, wenn Sie nichts zurückgeben, effektiv ein gelöstes Versprechen für den Wert zurückgeben undefined.

Alan Plum
quelle
137
Dieser Teil ist Gold: if you don't return anything, you are effectively returning a resolved promise for the value undefined.Danke @pluma
Valerio
7
Das ist in der Tat. Ich bearbeite es, um ihm den
Mut
Beendet die Ablehnung die aktuelle Funktion? zB wird die Auflösung nicht aufgerufen, wenn die Ablehnung als 1. bezeichnet wird, wenn (schlecht) {ablehnen (Status); } Entschlossenheit (Ergebnisse); `
SuperUberDuper
stepOne().then(stepTwo, handleErrorOne) `stepOne (). then (null, handleErrorOne) .then (stepTwo)` Sind diese wirklich äquivalent? Ich denke, im Falle einer Ablehnung wird in stepOneder zweiten Codezeile ausgeführt, stepTwoaber die erste wird nur ausgeführt handleErrorOneund gestoppt. Oder fehlt mir etwas?
JeFf
5
Bietet nicht wirklich eine klare Lösung für die gestellte Frage, dennoch eine gute Erklärung
Yerken
57

Etwas spät zur Party, aber diese einfache Lösung hat bei mir funktioniert:

function chainError(err) {
  return Promise.reject(err)
};

stepOne()
.then(stepTwo, chainError)
.then(stepThreee, chainError);

Dadurch können Sie aus der Kette ausbrechen .

Vinnyq12
quelle
1
Hat mir aber .then(user => { if (user) return Promise.reject('The email address already exists.') })
Craig van Tonder
1
@CraigvanTonder können Sie einfach innerhalb eines Versprechens werfen und es wird genauso funktionieren wie Ihr Code:.then(user => { if (user) throw 'The email address already exists.' })
Francisco Presencia
1
Dies ist die einzig richtige Antwort. Andernfalls wird Schritt 3 weiterhin ausgeführt, auch wenn Schritt 1 einen Fehler aufweist.
wdetac
1
Nur um zu verdeutlichen, wenn in stepOne () ein Fehler auftritt, werden beide chainError aufgerufen, oder? Wenn dies wünschenswert ist. Ich habe ein Snippet, das dies tut, nicht sicher, ob ich etwas falsch verstanden habe - runkit.com/embed/9q2q3rjxdar9
user320550
10

Was Sie brauchen, ist eine sich wiederholende .then()Kette mit einem Sonderfall zum Starten und einem Sonderfall zum Beenden.

Der Kniff besteht darin, die Schrittnummer des Fehlerfalls an einen endgültigen Fehlerbehandler weiterzuleiten.

  • Start: step(1)bedingungslos anrufen .
  • Wiederholtes Muster: Kette a .then()mit folgenden Rückrufen:
    • Erfolg: Schritt aufrufen (n + 1)
    • Fehler: Wirf den Wert, mit dem der zuvor zurückgestellte zurückgewiesen wurde, oder wirf den Fehler erneut.
  • Ende: Verketten Sie einen .then()Handler ohne Erfolg und einen letzten Fehlerhandler.

Sie können das Ganze in Langschrift schreiben, aber es ist einfacher, das Muster mit benannten, verallgemeinerten Funktionen zu demonstrieren:

function nextStep(n) {
    return step(n + 1);
}

function step(n) {
    console.log('step ' + n);
    var deferred = $q.defer();
    (n === 3) ? deferred.reject(n) : deferred.resolve(n);
    return deferred.promise;
}

function stepError(n) {
    throw(n);
}

function finalError(n) {
    console.log('finalError ' + n);
}
step(1)
    .then(nextStep, stepError)
    .then(nextStep, stepError)
    .then(nextStep, stepError)
    .then(nextStep, stepError)
    .then(nextStep, stepError)
    .then(null, finalError);});

siehe Demo

Beachten Sie, wie in step(), der Aufgeschobene abgelehnt oder aufgelöst wird n, wodurch dieser Wert den Rückrufen im nächsten Teil .then()der Kette zur Verfügung steht. Einmal stepErroraufgerufen, wird der Fehler wiederholt erneut ausgelöst, bis er von behandelt wird finalError.

Rote Beete-Rote Beete
quelle
Eine informative Antwort, es lohnt sich also, sie beizubehalten, aber das ist nicht das Problem, mit dem ich konfrontiert bin. Ich erwähne diese Lösung in meinem Beitrag und es ist nicht das, wonach ich suche. Siehe die Demo oben in meinem Beitrag.
m59
1
m59, dies ist eine Antwort auf die gestellte Frage: "Wie schreibe ich Versprechen, damit ich bei Ablehnung eine Funktion aufrufen kann, ohne alle Funktionen in der Fehlerkette aufzurufen?" und der Titel der Frage "Brechen Sie die Versprechenskette und rufen Sie eine Funktion auf, die auf dem Schritt in der Kette basiert, in dem sie gebrochen (abgelehnt) ist"
Rote Beete-Rote Beete
Richtig, wie gesagt, es ist informativ und ich habe diese Lösung sogar in meinen Beitrag aufgenommen (mit weniger Details). Dieser Ansatz dient dazu, Dinge so zu reparieren, dass die Kette fortgesetzt werden kann. Es kann zwar das erreichen, wonach ich suche, ist aber nicht so natürlich wie der Ansatz in der akzeptierten Antwort. Mit anderen Worten, wenn Sie das tun möchten, was durch den Titel und die gestellte Frage ausgedrückt wird, gehen Sie plumas Ansatz vor.
m59
7

Wenn Sie ablehnen, sollten Sie einen Ablehnungsfehler übergeben und dann die Schrittfehlerbehandlungsroutinen in eine Funktion einschließen, die prüft, ob die Zurückweisung bis zum Ende der Kette verarbeitet oder "erneut geworfen" werden soll:

// function mocking steps
function step(i) {
    i++;
    console.log('step', i);
    return q.resolve(i);
}

// function mocking a failing step
function failingStep(i) {
    i++;
    console.log('step '+ i + ' (will fail)');
    var e = new Error('Failed on step ' + i);
    e.step = i;
    return q.reject(e);
}

// error handler
function handleError(e){
    if (error.breakChain) {
        // handleError has already been called on this error
        // (see code bellow)
        log('errorHandler: skip handling');
        return q.reject(error);
    }
    // firs time this error is past to the handler
    console.error('errorHandler: caught error ' + error.message);
    // process the error 
    // ...
    //
    error.breakChain = true;
    return q.reject(error);
}

// run the steps, will fail on step 4
// and not run step 5 and 6
// note that handleError of step 5 will be called
// but since we use that error.breakChain boolean
// no processing will happen and the error will
// continue through the rejection path until done(,)

  step(0) // 1
  .catch(handleError)
  .then(step) // 2
  .catch(handleError)
  .then(step) // 3
  .catch(handleError)
  .then(failingStep)  // 4 fail
  .catch(handleError)
  .then(step) // 5
  .catch(handleError)
  .then(step) // 6
  .catch(handleError)
  .done(function(){
      log('success arguments', arguments);
  }, function (error) {
      log('Done, chain broke at step ' + error.step);
  });

Was Sie auf der Konsole sehen würden:

step 1
step 2
step 3
step 4 (will fail)
errorHandler: caught error 'Failed on step 4'
errorHandler: skip handling
errorHandler: skip handling
Done, chain broke at step 4

Hier ist ein Arbeitscode https://jsfiddle.net/8hzg5s7m/3/

Wenn Sie für jeden Schritt eine bestimmte Behandlung haben, könnte Ihr Wrapper wie folgt aussehen:

/*
 * simple wrapper to check if rejection
 * has already been handled
 * @param function real error handler
 */
function createHandler(realHandler) {
    return function(error) {
        if (error.breakChain) {
            return q.reject(error);
        }
        realHandler(error);
        error.breakChain = true;
        return q.reject(error);    
    }
}

dann deine Kette

step1()
.catch(createHandler(handleError1Fn))
.then(step2)
.catch(createHandler(handleError2Fn))
.then(step3)
.catch(createHandler(handleError3Fn))
.done(function(){
    log('success');
}, function (error) {
    log('Done, chain broke at step ' + error.step);
});
redben
quelle
2

Wenn ich das richtig verstehe, soll nur der Fehler für den fehlgeschlagenen Schritt angezeigt werden, oder?

Das sollte so einfach sein, wie den Fehlerfall des ersten Versprechens dahingehend zu ändern:

step(1).then(function (response) {
    step(2);
}, function (response) {
    stepError(1);
    return response;
}).then( ... )

Wenn $q.reject()Sie im Fehlerfall des ersten Schritts zurückkehren , lehnen Sie dieses Versprechen ab, wodurch der errorCallback im zweiten aufgerufen wird then(...).

Zajn
quelle
Was in aller Welt ... genau das habe ich getan! Sehen Sie in meinem Beitrag, dass ich das versucht habe, aber die Kette würde zurücktreten und rennen step(2). Jetzt habe ich es einfach noch einmal versucht, es passiert nicht. Ich bin so verwirrt.
m59
1
Ich habe gesehen, dass du das erwähnt hast. Das ist aber bizarr. Diese enthaltene Funktion return step(2);sollte immer nur aufgerufen werden, wenn sie step(1)erfolgreich aufgelöst wurde.
Zajn
Scratch that - es passiert definitiv. Wie ich in meinem Beitrag sagte, wenn Sie nicht verwenden return $q.reject(), wird die Kette weiterlaufen. In diesem Fall return responsevermasselt. Siehe dies: jsbin.com/EpaZIsIp/6/edit
m59
Hmm okay. Es scheint in dem jsbin zu funktionieren, den Sie gepostet haben, als ich das geändert habe, aber ich muss etwas verpasst haben.
Zajn
Ja, ich sehe definitiv, dass das jetzt nicht funktioniert. Zurück zum Zeichenbrett für mich!
Zajn
2
var s = 1;
start()
.then(function(){
    return step(s++);
})
.then(function() {
    return step(s++);
})
.then(function() {
    return step(s++);
})
.then(0, function(e){
   console.log(s-1); 
});

http://jsbin.com/EpaZIsIp/20/edit

Oder für eine beliebige Anzahl von Schritten automatisiert:

var promise = start();
var s = 1;
var l = 3;
while(l--) {
    promise = promise.then(function() {
        return step(s++);
    });
}
promise.then(0, function(e){
   console.log(s-1); 
});

http://jsbin.com/EpaZIsIp/21/edit

Esailija
quelle
Aber wenn deferred.reject(n)ich anrufe,
bekomme
2

Versuchen Sie, dies wie libs zu verwenden:

https://www.npmjs.com/package/promise-chain-break

    db.getData()
.then(pb((data) => {
    if (!data.someCheck()) {
        tellSomeone();

        // All other '.then' calls will be skiped
        return pb.BREAK;
    }
}))
.then(pb(() => {
}))
.then(pb(() => {
}))
.catch((error) => {
    console.error(error);
});
Leonid
quelle
2

Wenn Sie dieses Problem mit async / await lösen möchten:

(async function(){    
    try {        
        const response1, response2, response3
        response1 = await promise1()

        if(response1){
            response2 = await promise2()
        }
        if(response2){
            response3 = await promise3()
        }
        return [response1, response2, response3]
    } catch (error) {
        return []
    }

})()
Luispa
quelle
1

Fügen Sie Fehlerbehandlungsroutinen als separate Kettenelemente direkt zur Ausführung der Schritte hinzu:

        // Handle errors for step(1)
step(1).then(null, function() { stepError(1); return $q.reject(); })
.then(function() {
                 // Attach error handler for step(2),
                 // but only if step(2) is actually executed
  return step(2).then(null, function() { stepError(2); return $q.reject(); });
})
.then(function() {
                 // Attach error handler for step(3),
                 // but only if step(3) is actually executed
  return step(3).then(null, function() { stepError(3); return $q.reject(); });
});

oder mit catch():

       // Handle errors for step(1)
step(1).catch(function() { stepError(1); return $q.reject(); })
.then(function() {
                 // Attach error handler for step(2),
                 // but only if step(2) is actually executed
  return step(2).catch(function() { stepError(2); return $q.reject(); });
})
.then(function() {
                 // Attach error handler for step(3),
                 // but only if step(3) is actually executed
  return step(3).catch(function() { stepError(3); return $q.reject(); });
});

Hinweis: Dies ist im Grunde das gleiche Muster, das Pluma in seiner Antwort vorschlägt, jedoch unter Verwendung der Benennung des OP.

Zünder
quelle
1

Gefundene Promise.prototype.catch()Beispiele auf MDN unten sehr hilfreich.

(Die akzeptierte Antwort erwähnt, then(null, onErrorHandler)was im Grunde das gleiche ist wie catch(onErrorHandler).)

Verwenden und Verketten der Fangmethode

var p1 = new Promise(function(resolve, reject) {
  resolve('Success');
});

p1.then(function(value) {
  console.log(value); // "Success!"
  throw 'oh, no!';
}).catch(function(e) {
  console.log(e); // "oh, no!"
}).then(function(){
  console.log('after a catch the chain is restored');
}, function () {
  console.log('Not fired due to the catch');
});

// The following behaves the same as above
p1.then(function(value) {
  console.log(value); // "Success!"
  return Promise.reject('oh, no!');
}).catch(function(e) {
  console.log(e); // "oh, no!"
}).then(function(){
  console.log('after a catch the chain is restored');
}, function () {
  console.log('Not fired due to the catch');
});

Fallstricke beim Werfen von Fehlern

// Throwing an error will call the catch method most of the time
var p1 = new Promise(function(resolve, reject) {
  throw 'Uh-oh!';
});

p1.catch(function(e) {
  console.log(e); // "Uh-oh!"
});

// Errors thrown inside asynchronous functions will act like uncaught errors
var p2 = new Promise(function(resolve, reject) {
  setTimeout(function() {
    throw 'Uncaught Exception!';
  }, 1000);
});

p2.catch(function(e) {
  console.log(e); // This is never called
});

// Errors thrown after resolve is called will be silenced
var p3 = new Promise(function(resolve, reject) {
  resolve();
  throw 'Silenced Exception!';
});

p3.catch(function(e) {
   console.log(e); // This is never called
});

Wenn es gelöst ist

//Create a promise which would not call onReject
var p1 = Promise.resolve("calling next");

var p2 = p1.catch(function (reason) {
    //This is never called
    console.log("catch p1!");
    console.log(reason);
});

p2.then(function (value) {
    console.log("next promise's onFulfilled"); /* next promise's onFulfilled */
    console.log(value); /* calling next */
}, function (reason) {
    console.log("next promise's onRejected");
    console.log(reason);
});
toraritte
quelle
1

Die beste Lösung besteht darin, Ihre Versprechenskette umzugestalten, um ES6 zu verwenden. Dann können Sie einfach von der Funktion zurückkehren, um den Rest des Verhaltens zu überspringen.

Ich habe über ein Jahr lang meinen Kopf gegen dieses Muster geschlagen und mit Warten ist es der Himmel.

Pete Alvin
quelle
Bei Verwendung von reinem IE wird async / await nicht unterstützt.
ndee
0

Verwenden Sie ein SequentialPromise-Modul

Absicht

Stellen Sie ein Modul bereit, dessen Aufgabe es ist, Anforderungen nacheinander auszuführen, während der aktuelle Index jeder Operation auf ordinale Weise verfolgt wird. Definieren Sie die Operation aus Gründen der Flexibilität in einem Befehlsmuster .

Teilnehmer

  • Kontext : Das Objekt, dessen Mitgliedsmethode eine Operation ausführt.
  • SequentialPromise : Definiert eine executeMethode zum Verketten und Verfolgen jeder Operation. SequentialPromise gibt eine Versprechen-Kette von allen ausgeführten Operationen zurück.
  • Invoker : Erstellt eine SequentialPromise-Instanz, die Kontext und Aktion bereitstellt, und ruft ihre executeMethode auf, während eine ordinale Liste von Optionen für jede Operation übergeben wird.

Folgen

Verwenden Sie SequentialPromise, wenn das ordinale Verhalten der Promise-Auflösung erforderlich ist. SequentialPromise verfolgt den Index, für den ein Versprechen abgelehnt wurde.

Implementierung

clear();

var http = {
    get(url) {
        var delay = Math.floor( Math.random() * 10 ), even = !(delay % 2);
        var xhr = new Promise(exe);

        console.log(`REQUEST`, url, delay);
        xhr.then( (data) => console.log(`SUCCESS: `, data) ).catch( (data) => console.log(`FAILURE: `, data) );

        function exe(resolve, reject) {
            var action = { 'true': reject, 'false': resolve }[ even ];
            setTimeout( () => action({ url, delay }), (1000 * delay) );
        }

        return xhr;
    }
};

var SequentialPromise = new (function SequentialPromise() {
    var PRIVATE = this;

    return class SequentialPromise {

        constructor(context, action) {
            this.index = 0;
            this.requests = [ ];
            this.context = context;
            this.action = action;

            return this;
        }

        log() {}

        execute(url, ...more) {
            var { context, action, requests } = this;
            var chain = context[action](url);

            requests.push(chain);
            chain.then( (data) => this.index += 1 );

            if (more.length) return chain.then( () => this.execute(...more) );
            return chain;
        }

    };
})();

var sequence = new SequentialPromise(http, 'get');
var urls = [
    'url/name/space/0',
    'url/name/space/1',
    'url/name/space/2',
    'url/name/space/3',
    'url/name/space/4',
    'url/name/space/5',
    'url/name/space/6',
    'url/name/space/7',
    'url/name/space/8',
    'url/name/space/9'
];
var chain = sequence.execute(...urls);
var promises = sequence.requests;

chain.catch( () => console.warn(`EXECUTION STOPPED at ${sequence.index} for ${urls[sequence.index]}`) );

// console.log('>', chain, promises);

Kern

SequentialPromise

Cody
quelle
0

Wenn Sie irgendwann zurückkehren, werden Promise.reject('something')Sie in den Fangblock zum Versprechen geworfen.

promiseOne
  .then((result) => {
    if (!result) {
      return Promise.reject('No result');
    }
    return;
  })
  .catch((err) => {
    console.log(err);
  });

Wenn das erste Versprechen kein Ergebnis zurückgibt, wird in der Konsole nur "Kein Ergebnis" angezeigt.

Dimitar Gospodinov
quelle