Quebre a cadeia da promessa e chame uma função com base na etapa da cadeia em que está quebrada (rejeitada)

135

Atualizar:

Para ajudar os futuros telespectadores deste post, criei esta demonstração da resposta da pluma .

Questão:

Meu objetivo parece bastante direto.

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

O problema aqui é que, se eu falhar na etapa 1, ambos stepError(1)AND stepError(2)serão acionados. Se não o fizer return $q.reject, stepError(2)não serei demitido, mas step(2)sim, o que eu entendo. Eu realizei tudo, exceto o que estou tentando fazer.

Como escrevo promessas para poder chamar uma função de rejeição, sem chamar todas as funções na cadeia de erros? Ou existe outra maneira de conseguir isso?

Aqui está uma demonstração ao vivo para que você possa trabalhar com algo.

Atualizar:

Eu meio que resolvi isso. Aqui, estou capturando o erro no final da cadeia e passando os dados para, reject(data)para saber o problema a ser tratado na função de erro. Na verdade, isso não atende aos meus requisitos, porque não quero depender dos dados. Seria ruim, mas no meu caso, seria mais limpo passar um retorno de chamada de erro para a função, em vez de depender dos dados retornados para determinar o que fazer.

Demonstração ao vivo aqui (clique).

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
fonte
1
Há um assíncrono javascript lib que pode ajudar se isso se torna mais complicado
lucuma
Promise.prototype.catch()exemplos no MDN mostram a solução para exatamente os mesmos problemas.
toraritte

Respostas:

199

O motivo pelo qual seu código não funciona conforme o esperado é que ele está realmente fazendo algo diferente do que você pensa que faz.

Digamos que você tenha algo como o seguinte:

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

Para entender melhor o que está acontecendo, vamos fingir que este é um código síncrono com 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);
}

O onRejectedmanipulador (o segundo argumento de then) é essencialmente um mecanismo de correção de erros (como um catchbloco). Se um erro for lançado handleErrorOne, ele será capturado pelo próximo bloco de captura ( catch(e2)) e assim por diante.

Obviamente, isso não é o que você pretendia.

Digamos que queremos que toda a cadeia de resolução falhe, não importa o que dê errado:

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

Nota: Podemos deixar o local handleErrorOneonde está, porque ele será invocado apenas se for stepOnerejeitado (é a primeira função na cadeia, portanto, sabemos que se a cadeia for rejeitada neste momento, ela poderá ser apenas por causa da promessa dessa função) .

A mudança importante é que os manipuladores de erro para as outras funções não fazem parte da principal cadeia de promessas. Em vez disso, cada etapa tem sua própria "sub-cadeia" com uma onRejectedque é chamada apenas se a etapa foi rejeitada (mas não pode ser alcançada diretamente pela cadeia principal).

A razão pela qual isso funciona é que ambos onFulfillede onRejectedsão argumentos opcionais para o thenmétodo. Se uma promessa for cumprida (ou seja, resolvida) e a próxima thenda cadeia não tiver um onFulfilledmanipulador, a cadeia continuará até que exista uma com esse manipulador.

Isso significa que as duas linhas a seguir são equivalentes:

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

Mas a seguinte linha não é equivalente às duas acima:

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

A biblioteca de promessas do Angular $qé baseada na Qbiblioteca do kriskowal (que possui uma API mais rica, mas contém tudo o que você pode encontrar $q). Os documentos da API da Q no GitHub podem ser úteis. Q implementa a especificação Promises / A + , que detalha como thene o comportamento da resolução da promessa funciona exatamente.

EDITAR:

Lembre-se também de que, se você quiser sair da cadeia em seu manipulador de erros, ele precisará retornar uma promessa rejeitada ou lançar um erro (que será capturado e envolvido automaticamente em uma promessa rejeitada). Se você não retornar uma promessa, thenagrupe o valor de retorno em uma promessa de resolução para você.

Isso significa que, se você não devolver nada, estará efetivamente retornando uma promessa resolvida para o valor undefined.

Alan Plum
fonte
138
Esta parte é ouro: if you don't return anything, you are effectively returning a resolved promise for the value undefined.Thanks @pluma
Valerio
7
Isto é de fato. Estou editando-lo para dar-lhe a negrito que merece
Cyril CHAPON
rejeita sair da função atual? por exemplo, determinação não será chamada se rejeitar for chamado 1st `if (bad) {rejeitar (status); } resolve (resultados); `
SuperUberDuper
stepOne().then(stepTwo, handleErrorOne) `stepOne (). then (null, handleErrorOne) .then (stepTwo)` Estes são realmente equivalentes? Eu acho que em caso de rejeição na stepOnesegunda linha de código será executado, stepTwomas o primeiro só será executado handleErrorOnee interrompido. Ou eu estou esquecendo de alguma coisa?
JeFf 28/01
5
Realmente não fornecer uma solução clara para a pergunta feita, boa explicação, no entanto
Yerken
57

Um pouco atrasado para a festa, mas esta solução simples funcionou para mim:

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

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

Isso permite que você quebrar fora da cadeia.

Vinnyq12
fonte
1
Ajudou-me, mas, para sua informação, você pode devolvê-lo no momento para começar a captura como: .then(user => { if (user) return Promise.reject('The email address already exists.') })
Craig van Tonder
1
@CraigvanTonder você pode apenas jogar dentro de uma promessa e ele vai trabalhar o mesmo que o código sua:.then(user => { if (user) throw 'The email address already exists.' })
Francisco Presencia
1
Esta é a única resposta correta. Caso contrário, a etapa 3 ainda será executada, mesmo a etapa 1 terá erro.
Wdetac
1
Só para esclarecer, se ocorrer um erro no stepOne (), o chainError será invocado, certo? Se isso é desejável. Eu tenho um snippet que faz isso, não tenho certeza se entendi
user320550
10

O que você precisa é de uma .then()cadeia de repetição com um caso especial para começar e um caso especial para terminar.

A habilidade é fazer com que o número da etapa do caso de falha passe para um manipulador de erros final.

  • Início: ligue step(1)incondicionalmente.
  • Padrão de repetição: encadeie a .then()com os seguintes retornos de chamada:
    • sucesso: etapa da chamada (n + 1)
    • falha: lance o valor com o qual o adiado anterior foi rejeitado ou repita o erro.
  • Concluir: encadeie um .then()sem manipulador de sucesso e um manipulador de erro final.

Você pode escrever a coisa toda à mão, mas é mais fácil demonstrar o padrão com funções generalizadas nomeadas:

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

Vejo demonstração

Observe como step(), o adiado é rejeitado ou resolvido n, tornando esse valor disponível para os retornos de chamada no próximo .then()na cadeia. Uma vez stepErrorchamado, o erro é repetidamente repetido até ser tratado por finalError.

Beterraba-beterraba
fonte
Resposta informativa, por isso vale a pena manter, mas esse não é o problema que estou enfrentando. Menciono esta solução no meu post e não é o que estou procurando. Veja a demonstração no topo da minha postagem.
m59 21/12/13
1
m59, essa é uma resposta à pergunta: "como escrevo promessas para poder chamar uma função de rejeição, sem chamar todas as funções da cadeia de erros?" eo título da pergunta, "cadeia promessa Break e chamar uma função baseada na etapa da cadeia, onde ele está quebrado (rejeitada)"
Beterraba-Beterraba
Certo, como eu disse, é informativo e até incluí esta solução no meu post (com menos detalhes). Essa abordagem destina-se a consertar as coisas para que a cadeia possa continuar. Embora possa realizar o que estou procurando, não é tão natural quanto a abordagem na resposta aceita. Em outras palavras, se você quiser fazer o que é expresso pelo título e pela pergunta, adote a abordagem da pluma.
M59
7

Ao rejeitar, você deve passar um erro de rejeição e agrupar manipuladores de erro de etapa em uma função que verifica se a rejeição deve ser processada ou "retrocedida" até o final da cadeia:

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

O que você veria no 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

Aqui está um código de trabalho https://jsfiddle.net/8hzg5s7m/3/

Se você tiver um tratamento específico para cada etapa, seu wrapper pode ser algo como:

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

então sua corrente

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
fonte
2

Se bem entendi, você deseja que apenas seja exibido o erro da etapa que falhou, certo?

Isso deve ser tão simples quanto mudar o caso de falha da primeira promessa para isso:

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

Ao retornar $q.reject()no caso de falha da primeira etapa, você está rejeitando essa promessa, que faz com que o errorCallback seja chamado na 2ª then(...).

Zajn
fonte
O que no mundo ... foi exatamente o que eu fiz! Veja no meu post que tentei isso, mas a corrente recuava e corria step(2). Agora eu tentei novamente, isso não está acontecendo. Estou tão confuso.
M59
1
Eu vi que você mencionou isso. Isso é bizarro. Essa função que contém return step(2);apenas deve ser chamada sempre que for step(1)resolvida com êxito.
Zajn
Risque isso - definitivamente está acontecendo. Como eu disse no meu post, se você não usar return $q.reject(), a corrente continuará. Neste caso, return responseestraguei tudo. Veja isto: jsbin.com/EpaZIsIp/6/edit
m59
Hmm OK. Parece funcionar no jsbin que você postou quando mudei isso, mas devo ter perdido alguma coisa.
Zajn
Sim, eu definitivamente vejo que não está funcionando agora. De volta à prancheta para mim!
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

Ou automatizado para qualquer número de etapas:

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
fonte
Mas se eu chamará deferred.reject(n)então eu estou recebendo aviso de que promessa rejeitado com um objeto nonError
9me
2

Tente ro usar isso como 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);
});
Leonid
fonte
2

Se você deseja resolver esse problema usando async / waitit:

(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
fonte
1

Anexe manipuladores de erro como elementos de cadeia separados diretamente à execução das etapas:

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

ou 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: Esse é basicamente o mesmo padrão sugerido pela pluma em sua resposta, mas usando o nome do OP.

Ignitor
fonte
1

Promise.prototype.catch()Exemplos encontrados no MDN abaixo são muito úteis.

(A resposta aceita menciona then(null, onErrorHandler)que é basicamente o mesmo que catch(onErrorHandler).)

Usando e encadeando o método catch

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

Pegadinhas ao lançar erros

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

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

A melhor solução é refatorar sua cadeia de promessas para usar os ES6 aguardados. Em seguida, você pode simplesmente retornar da função para pular o restante do comportamento.

Eu tenho batido minha cabeça contra esse padrão por mais de um ano e usar a espera é o paraíso.

Pete Alvin
fonte
Ao usar o IE puro assíncrono / aguardar, não há suporte.
Ndee 29/11/19
0

Use um módulo SequentialPromise

Intenção

Forneça um módulo cuja responsabilidade seja executar solicitações sequencialmente, enquanto rastreia o índice atual de cada operação de maneira ordinal. Defina a operação em um Padrão de Comando para flexibilidade.

Participantes

  • Contexto : o objeto cujo método membro executa uma operação.
  • SequentialPromise : define um executemétodo para encadear e rastrear cada operação. SequentialPromise retorna uma cadeia de promessas de todas as operações executadas.
  • Invoker : Cria uma instância SequentialPromise, fornecendo contexto e ação, e chama seu executemétodo enquanto passa uma lista ordinal de opções para cada operação.

Consequências

Use SequentialPromise quando o comportamento ordinal da resolução Promise for necessário. SequentialPromise rastreará o índice para o qual uma promessa foi rejeitada.

Implementação

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

Essência

SequentialPromise

Cody
fonte
0

Se em algum momento você retornar, Promise.reject('something')você será jogado no bloco de captura da promessa.

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

Se a primeira promessa não retornar nenhum resultado, você obterá apenas 'Sem resultado' no console.

Dimitar Gospodinov
fonte