Interrompi la catena di promesse e chiama una funzione basata sul passaggio nella catena in cui è interrotta (rifiutata)


135

Aggiornare:

Per aiutare i futuri spettatori di questo post, ho creato questa demo della risposta di Pluma .

Domanda:

Il mio obiettivo sembra abbastanza semplice.

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

Il problema qui è che se fallisco nel passaggio 1, entrambi stepError(1)AND stepError(2)vengono attivati. Se non lo faccio return $q.rejectallorastepError(2) non saranno licenziati, ma step(2)volontà, che ho capito. Ho realizzato tutto tranne ciò che sto cercando di fare.

Come faccio a scrivere promesse per poter chiamare una funzione in caso di rifiuto, senza chiamare tutte le funzioni nella catena di errori? O c'è un altro modo per farlo?

Ecco una demo dal vivo quindi hai qualcosa con cui lavorare.

Aggiornare:

Ho sorta di aver risolto. Qui, sto rilevando l'errore alla fine della catena e passando i dati in reject(data)modo da sapere quale problema gestire nella funzione di errore. Questo in realtà non soddisfa i miei requisiti perché non voglio dipendere dai dati. Sarebbe zoppo, ma nel mio caso sarebbe più pulito passare un callback di errore alla funzione piuttosto che dipendere dai dati restituiti per determinare cosa fare.

Demo live qui (clicca).

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

1
Esiste una lib async javascript che potrebbe aiutare se questo diventa più complicato
lucuma

Promise.prototype.catch()esempi su MDN mostrano la soluzione per gli stessi identici problemi.
Toraritte,

Risposte:


199

Il motivo per cui il tuo codice non funziona come previsto è che sta effettivamente facendo qualcosa di diverso da quello che pensi che funzioni.

Supponiamo che tu abbia qualcosa di simile al seguente:

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

Per capire meglio cosa sta succedendo, facciamo finta che questo sia un codice sincrono con try/ catchblocks:

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

Il onRejectedgestore (il secondo argomento di then) è essenzialmente un meccanismo di correzione degli errori (come un catchblocco). Se viene generato un errore handleErrorOne, verrà catturato dal blocco di cattura successivo (catch(e2) ) e così via.

Questo ovviamente non è quello che volevi.

Diciamo che vogliamo che l'intera catena di risoluzione fallisca, qualunque cosa vada storta:

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

Nota: possiamo lasciare handleErrorOnedove si trova, perché verrà invocato solo sestepOne rifiuta (è la prima funzione nella catena, quindi sappiamo che se la catena viene rifiutata a questo punto, può essere solo a causa della promessa di quella funzione) .

Il cambiamento importante è che i gestori di errori per le altre funzioni non fanno parte della catena di promesse principale. Invece, ogni passaggio ha la sua "sotto-catena" con unonRejected che viene chiamato solo se il passo è stato rifiutato (ma non può essere raggiunto direttamente dalla catena principale).

Il motivo per cui funziona è che entrambi onFulfillede onRejectedsono argomenti opzionali al thenmetodo. Se una promessa viene adempiuta (cioè risolta) e il successivo thennella catena non ha un onFulfilledgestore, la catena continuerà fino a quando non ce ne sarà una con un tale gestore.

Ciò significa che le seguenti due righe sono equivalenti:

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

Ma la seguente riga non equivale alle due sopra:

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

La libreria di promesse di Angular $qsi basa sulla Qlibreria di kriskowal (che ha un'API più ricca, ma contiene tutto ciò che puoi trovare $q). I documenti API di Q su GitHub potrebbero rivelarsi utili. Q implementa la specifica Promises / A + , che spiega in dettaglio comethen e esattamente il comportamento della risoluzione delle promesse.

MODIFICARE:

Inoltre, tieni presente che se desideri uscire dalla catena nel tuo gestore degli errori, deve restituire una promessa rifiutata o lanciare un errore (che verrà catturato e racchiuso automaticamente in una promessa rifiutata). Se non restituisci una promessa, thenracchiudi il valore restituito in una promessa risolutiva per te.

Ciò significa che se non si restituisce nulla, si sta effettivamente restituendo una promessa risolta per il valore undefined.


138
Questa parte è d'oro: if you don't return anything, you are effectively returning a resolved promise for the value undefined.grazie @pluma
Valerio

7
Questo è davvero. Lo sto modificando per dargli il coraggio che merita
Cyril CHAPON

Rifiuta esce dalla funzione corrente? ad esempio, la risoluzione non verrà chiamata se il rifiuto viene chiamato 1st `if (errato) {rifiuta (stato); } risoluzione (risultati); `
SuperUberDuper

stepOne().then(stepTwo, handleErrorOne) `stepOne (). then (null, handleErrorOne) .then (stepTwo)` Sono veramente equivalenti? Penso che in caso di rifiuto stepOneverrà eseguita la seconda riga di codicestepTwo ma la prima verrà eseguita handleErrorOnee arrestata. Oppure mi sfugge qualcosa?
JeFf,

5
In realtà non fornisce una soluzione chiara alla domanda posta, buona spiegazione comunque
Yerken

57

Un po 'tardi alla festa, ma questa semplice soluzione ha funzionato per me:

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

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

Questo ti permette di rompere fuori della catena.


1
Mi ha aiutato, ma FYI, puoi restituirlo in poi per sfondare il pescato come:.then(user => { if (user) return Promise.reject('The email address already exists.') })
Craig van Tonder,

1
@CraigvanTonder puoi semplicemente lanciare una promessa e funzionerà come il tuo codice:.then(user => { if (user) throw 'The email address already exists.' })
Francisco Presencia,

1
Questa è l'unica risposta corretta. In caso contrario, il passaggio 3 verrà comunque eseguito anche se il passaggio 1 presenta errori.
wdetac,

1
Solo per chiarire, se si verifica un errore in stepOne (), allora sia chainError viene invocato giusto? Se questo è desiderabile. Ho uno snippet che lo fa, non sono sicuro di aver frainteso qualcosa - runkit.com/embed/9q2q3rjxdar9
user320550

10

Ciò di cui hai bisogno è una ripetizione .then() catena che si con un caso speciale per iniziare e un caso speciale per finire.

L'abilità è quella di ottenere il numero di passaggio del caso di errore da passare a un gestore degli errori finale.

  • Inizio: chiama step(1) incondizionatamente.
  • Motivo ripetuto: catena a .then() con i seguenti callback:
    • successo: chiama il passaggio (n + 1)
    • fallimento: genera il valore con il quale il precedente differito è stato rifiutato o ricodifica l'errore.
  • Fine: catena .then()a senza gestore di successo e gestore di errori finale.

Puoi scrivere tutto a mano, ma è più facile dimostrare lo schema con funzioni nominate e generalizzate:

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

vedi demo

Notare come step(), il differito viene rifiutato o risolto n, rendendo così quel valore disponibile per i callback nel successivo .then()nella catena. Una volta stepErrorchiamato, l'errore viene ripetutamente riproposto fino a quando non viene gestito da finalError.


Risposta informativa quindi vale la pena conservarla, ma non è questo il problema che sto affrontando. Cito questa soluzione nel mio post e non è quello che sto cercando. Guarda la demo nella parte superiore del mio post.
m59,

1
m59, questa è una risposta alla domanda posta, "come faccio a scrivere promesse in modo da poter chiamare una funzione in caso di rifiuto, senza chiamare tutte le funzioni nella catena di errori?" e il titolo della domanda "Rompi la catena della promessa e chiama una funzione basata sul passaggio nella catena in cui è rotta (respinta)"
Beetroot-Beetroot,

Giusto, come ho detto, è informativo e ho anche incluso questa soluzione nel mio post (con meno dettagli). Questo approccio ha lo scopo di sistemare le cose in modo che la catena possa continuare. Mentre può realizzare ciò che sto cercando, non è naturale come l'approccio nella risposta accettata. In altre parole, se vuoi fare ciò che è espresso dal titolo e dalla domanda posta, prendi l'approccio di Pluma.
m59,

7

Quando si rifiuta è necessario passare un errore di rifiuto, quindi avvolgere i gestori degli errori di passaggio in una funzione che controlla se il rifiuto deve essere elaborato o "riprogrammato" fino alla fine della catena:

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

Cosa vedresti sulla console:

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

Ecco qualche codice funzionante https://jsfiddle.net/8hzg5s7m/3/

Se hai una gestione specifica per ogni passaggio, il tuo wrapper potrebbe essere qualcosa del tipo:

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

quindi la tua catena

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

2

Se ho capito bene, vuoi mostrare solo l'errore per il passaggio fallito, giusto?

Dovrebbe essere semplice come cambiare il caso di fallimento della prima promessa a questo:

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

Tornando $q.reject()nel caso di fallimento del primo passo, stai rifiutando quella promessa, che causa la chiamata a errorCallback nel 2 ° then(...).


Cosa nel mondo ... è esattamente quello che ho fatto! Vedi nel mio post che l'ho provato, ma la catena si riavvierebbe e correrebbe step(2). Ora ho appena provato di nuovo che non sta succedendo. Sono così confuso.
m59,

1
Ho visto che lo hai menzionato. È strano però. Quella funzione che contiene return step(2);dovrebbe essere sempre chiamata solo quando si step(1)risolve correttamente.
Zajn,

Grattalo - sta sicuramente accadendo. Come ho detto nel mio post, se non lo usi return $q.reject(), la catena continuerà. In questo caso è return responseincasinato. Vedi questo: jsbin.com/EpaZIsIp/6/edit
m59

Va bene. Sembra funzionare nel jsbin che hai pubblicato quando l'ho cambiato, ma devo aver perso qualcosa.
Zajn,

Sì, vedo sicuramente che non funziona ora. Torna al tavolo da disegno per me!
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

O automatizzato per qualsiasi numero di passaggi:

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


Ma se chiamerò, deferred.reject(n)allora riceverò un avviso che la promessa è stata respinta con un oggetto
nonError

2

Prova a usarlo come libs:

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

2

Se vuoi risolvere questo problema usando async / await:

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

})()

1

Collegare i gestori errori come elementi catena separati direttamente all'esecuzione dei passaggi:

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

o usando 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(); });
});

Nota: questo è fondamentalmente lo stesso modello suggerito da Pluma nella sua risposta, ma utilizzando la denominazione dell'OP.


1

Esempi trovati Promise.prototype.catch()su MDN seguito sono riportati molto utili .

(La risposta accettata menziona then(null, onErrorHandler)sostanzialmente lo stesso di catch(onErrorHandler).)

Utilizzo e concatenamento del metodo di cattura

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

Gotcha quando si generano errori

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

Se è stato risolto

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

1

La soluzione migliore è quella di refactoring alla catena di promesse per utilizzare ES6 waitit's. Quindi puoi semplicemente tornare dalla funzione per saltare il resto del comportamento.

Ho battuto la testa contro questo schema per oltre un anno e usare il waitit è il paradiso.


Quando si utilizza puro IE async / await non è supportato.
ndee,

0

Utilizzare un modulo SequentialPromise

Intenzione

Fornire un modulo la cui responsabilità è di eseguire le richieste in sequenza, tenendo traccia dell'indice corrente di ciascuna operazione in modo ordinale. Definire l'operazione in un modello di comando per flessibilità.

I partecipanti

  • Contesto : l'oggetto il cui metodo membro esegue un'operazione.
  • SequentialPromise : definisce un executemetodo per concatenare e tracciare ciascuna operazione. SequentialPromise restituisce una catena di promesse da tutte le operazioni eseguite.
  • Invoker : crea un'istanza SequentialPromise, fornendo contesto e azione e chiama il suo executemetodo mentre passa un elenco ordinale di opzioni per ogni operazione.

conseguenze

Utilizzare SequentialPromise quando è necessario il comportamento ordinale della risoluzione Promise. SequentialPromise seguirà l'indice per il quale una Promessa è stata rifiutata.

Implementazione

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

nocciolo

SequentialPromise


0

Se in qualsiasi momento torni Promise.reject('something'), verrai gettato nel blocco di cattura della promessa.

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

Se la prima promessa non restituisce alcun risultato, nella console verrà visualizzato solo "Nessun risultato" .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.