Manipulação de várias capturas na cadeia de promessas

125

Ainda sou bastante novo nas promessas e atualmente estou usando o bluebird, no entanto, tenho um cenário em que não tenho certeza de como lidar melhor com isso.

Por exemplo, eu tenho uma cadeia de promessas em um aplicativo expresso da seguinte forma:

repository.Query(getAccountByIdQuery)
        .catch(function(error){
            res.status(404).send({ error: "No account found with this Id" });
        })
        .then(convertDocumentToModel)
        .then(verifyOldPassword)
        .catch(function(error) {
            res.status(406).send({ OldPassword: error });
        })
        .then(changePassword)
        .then(function(){
            res.status(200).send();
        })
        .catch(function(error){
            console.log(error);
            res.status(500).send({ error: "Unable to change password" });
        });

Então, o comportamento que eu busco é:

  • Vai obter a conta por ID
  • Se houver uma rejeição neste momento, bombardeie e retorne um erro
  • Se não houver erro, converta o documento retornado para um modelo
  • Verifique a senha com o documento do banco de dados
  • Se as senhas não corresponderem, bombardeie e retorne um erro diferente
  • Se não houver erro, altere as senhas
  • Então retorne o sucesso
  • Se algo mais der errado, retorne 500

Portanto, atualmente, as capturas não parecem interromper o encadeamento, e isso faz sentido, por isso estou me perguntando se existe uma maneira de forçar de alguma forma a cadeia a parar em um determinado ponto com base nos erros ou se existe uma maneira melhor estruturar isso para obter algum tipo de comportamento de ramificação, como é o caso if X do Y else Z.

Qualquer ajuda seria ótimo.

Grofit
fonte
Você pode rever novamente ou retornar mais cedo?
precisa saber é o seguinte

Respostas:

126

Esse comportamento é exatamente como um lançamento síncrono:

try{
    throw new Error();
} catch(e){
    // handle
} 
// this code will run, since you recovered from the error!

Isso é metade do ponto de .catch- ser capaz de se recuperar de erros. Pode ser desejável tentar novamente para sinalizar que o estado ainda é um erro:

try{
    throw new Error();
} catch(e){
    // handle
    throw e; // or a wrapper over e so we know it wasn't handled
} 
// this code will not run

No entanto, isso por si só não funcionará no seu caso, pois o erro foi detectado por um manipulador posterior. O problema real aqui é que os manipuladores de erro generalizados "HANDLE ANYTHING" são uma prática ruim em geral e são extremamente desaprovados em outras linguagens de programação e ecossistemas. Por esse motivo, o Bluebird oferece capturas digitadas e predicadas.

A vantagem adicional é que sua lógica de negócios não precisa (nem deveria) estar ciente do ciclo de solicitação / resposta. Não é responsabilidade da consulta decidir qual status e erro HTTP o cliente recebe e, posteriormente, à medida que seu aplicativo cresce, você pode separar a lógica de negócios (como consultar seu banco de dados e como processar seus dados) do que você envia ao cliente (qual código de status http, qual texto e qual resposta).

Aqui está como eu escreveria seu código.

Primeiro, eu .Queryjogaria uma NoSuchAccountErrorsubclasse da Promise.OperationalErrorqual o Bluebird já fornece. Se você não souber como subclassificar um erro, entre em contato.

Além disso, eu a subclassificaria AuthenticationErrore faria algo como:

function changePassword(queryDataEtc){ 
    return repository.Query(getAccountByIdQuery)
                     .then(convertDocumentToModel)
                     .then(verifyOldPassword)
                     .then(changePassword);
}

Como você pode ver - é muito limpo e você pode ler o texto como um manual de instruções do que acontece no processo. Também é separado da solicitação / resposta.

Agora, eu chamaria isso do manipulador de rota como tal:

 changePassword(params)
 .catch(NoSuchAccountError, function(e){
     res.status(404).send({ error: "No account found with this Id" });
 }).catch(AuthenticationError, function(e){
     res.status(406).send({ OldPassword: error });
 }).error(function(e){ // catches any remaining operational errors
     res.status(500).send({ error: "Unable to change password" });
 }).catch(function(e){
     res.status(500).send({ error: "Unknown internal server error" });
 });

Dessa forma, a lógica está tudo em um só lugar e a decisão de como lidar com os erros do cliente está em um só lugar e eles não se confundem.

Benjamin Gruenbaum
fonte
11
Você pode adicionar que o motivo de ter um .catch(someSpecificError)manipulador intermediário para algum erro específico é se você deseja capturar um tipo específico de erro (que é inofensivo), lidar com ele e continuar o fluxo a seguir. Por exemplo, eu tenho algum código de inicialização que tem uma sequência de coisas a fazer. A primeira coisa é ler o arquivo de configuração do disco, mas se esse arquivo de configuração estiver faltando, será um erro OK (o programa possui padrões internos) para que eu possa lidar com esse erro específico e continuar o restante do fluxo. Também pode haver limpeza melhor para não sair até mais tarde.
precisa saber é o seguinte
1
Eu pensei que "Isso é metade do ponto do .catch - ser capaz de se recuperar de erros" deixou isso claro, mas obrigado por esclarecer ainda mais que é um bom exemplo.
Benjamin Gruenbaum
1
E se o bluebird não estiver sendo usado? As promessas simples do es6 têm apenas uma mensagem de erro de cadeia que é passada para captura.
clocksmith
3
O @clocksmith com ES6 promete que você está preso ao pegar tudo e executar instanceofchceks manualmente manualmente.
Benjamin Gruenbaum
1
Para quem procura uma referência para subclassificar objetos Error, leia bluebirdjs.com/docs/api/catch.html#filtered-catch . O artigo também reproduz praticamente a resposta múltipla apresentada aqui.
Mummybot # 30/17
47

.catchfunciona como a try-catchdeclaração, o que significa que você só precisa de uma captura no final:

repository.Query(getAccountByIdQuery)
        .then(convertDocumentToModel)
        .then(verifyOldPassword)
        .then(changePassword)
        .then(function(){
            res.status(200).send();
        })
        .catch(function(error) {
            if (/*see if error is not found error*/) {
                res.status(404).send({ error: "No account found with this Id" });
            } else if (/*see if error is verification error*/) {
                res.status(406).send({ OldPassword: error });
            } else {
                console.log(error);
                res.status(500).send({ error: "Unable to change password" });
            }
        });
Esailija
fonte
1
Sim, eu sabia disso, mas não queria criar uma enorme cadeia de erros, e parecia mais legível fazê-lo como e quando necessário. Daí a captura no final, mas eu gosto da idéia de erros digitados, pois é mais descritiva quanto à intenção.
Grofit 27/09/14
8
@ Grofit pelo que vale a pena digitar em Bluebird, foi a idéia de Petka (Esailija) para começar :) Não há necessidade de convencê-lo de que é uma abordagem preferível aqui. Acho que ele não queria confundir você, já que muitas pessoas em JS não estão muito conscientes do conceito.
Benjamin Gruenbaum 28/09
17

Eu estou querendo saber se existe uma maneira de forçar de alguma forma a cadeia a parar em um determinado ponto com base nos erros

Não. Você não pode realmente "terminar" uma corrente, a menos que você lance uma exceção que borbulha até o fim. Veja a resposta de Benjamin Gruenbaum para saber como fazer isso.

A derivação de seu padrão seria não para distinguir tipos de erro, mas usar erros que têm statusCodee bodycampos que podem ser enviados a partir de um único, genérico .catchmanipulador. Dependendo da estrutura da sua aplicação, a solução dele pode ser mais limpa.

ou se existe uma maneira melhor de estruturar isso para obter alguma forma de comportamento de ramificação

Sim, você pode fazer ramificações com promessas . No entanto, isso significa deixar a cadeia e "voltar" ao aninhamento - assim como você faria em uma declaração aninhada if-else ou try-catch:

repository.Query(getAccountByIdQuery)
.then(function(account) {
    return convertDocumentToModel(account)
    .then(verifyOldPassword)
    .then(function(verification) {
        return changePassword(verification)
        .then(function() {
            res.status(200).send();
        })
    }, function(verificationError) {
        res.status(406).send({ OldPassword: error });
    })
}, function(accountError){
    res.status(404).send({ error: "No account found with this Id" });
})
.catch(function(error){
    console.log(error);
    res.status(500).send({ error: "Unable to change password" });
});
Bergi
fonte
5

Eu tenho feito desta maneira:

Você deixa a sua pegada no final. E basta lançar um erro quando isso acontece no meio da sua cadeia.

    repository.Query(getAccountByIdQuery)
    .then((resultOfQuery) => convertDocumentToModel(resultOfQuery)) //inside convertDocumentToModel() you check for empty and then throw new Error('no_account')
    .then((model) => verifyOldPassword(model)) //inside convertDocumentToModel() you check for empty and then throw new Error('no_account')        
    .then(changePassword)
    .then(function(){
        res.status(200).send();
    })
    .catch((error) => {
    if (error.name === 'no_account'){
        res.status(404).send({ error: "No account found with this Id" });

    } else  if (error.name === 'wrong_old_password'){
        res.status(406).send({ OldPassword: error });

    } else {
         res.status(500).send({ error: "Unable to change password" });

    }
});

Suas outras funções provavelmente se pareceriam com isso:

function convertDocumentToModel(resultOfQuery) {
    if (!resultOfQuery){
        throw new Error('no_account');
    } else {
    return new Promise(function(resolve) {
        //do stuff then resolve
        resolve(model);
    }                       
}
Leo Leao
fonte
4

Provavelmente um pouco tarde para a festa, mas é possível aninhar .catchcomo mostrado aqui:

Mozilla Developer Network - Usando promessas

Editar: enviei isso porque ele fornece a funcionalidade solicitada em geral. No entanto, não neste caso particular. Porque, como explicado em detalhes por outros já, .catchdeve recuperar o erro. Você não pode, por exemplo, enviar uma resposta para o cliente em vários .catch retornos de chamada porque um .catchsem explícitas return resolve -lo com undefined, nesse caso, causando processo .thenpara acionar embora sua cadeia não é realmente resolvido, potencialmente causando uma sequência .catchde gatilho e enviando outra resposta ao cliente, causando um erro e provavelmente jogando do UnhandledPromiseRejectionseu jeito. Espero que essa sentença complicada faça algum sentido para você.

denkquer
fonte
1
@AntonMenshov Você está certo. I expandiu minha resposta, explicando por que seu comportamento desejado ainda não é possível com nidificação
denkquer
2

Em vez de .then().catch()...você pode fazer .then(resolveFunc, rejectFunc). Essa cadeia de promessas seria melhor se você lidasse com as coisas ao longo do caminho. Aqui está como eu o reescreveria:

repository.Query(getAccountByIdQuery)
    .then(
        convertDocumentToModel,
        () => {
            res.status(404).send({ error: "No account found with this Id" });
            return Promise.reject(null)
        }
    )
    .then(
        verifyOldPassword,
        () => Promise.reject(null)
    )
    .then(
        changePassword,
        (error) => {
            if (error != null) {
                res.status(406).send({ OldPassword: error });
            }
            return Promise.Promise.reject(null);
        }
    )
    .then(
        _ => res.status(200).send(),
        error => {
            if (error != null) {
                console.error(error);
                res.status(500).send({ error: "Unable to change password" });
            }
        }
    );

Nota: A if (error != null)é um pouco de um truque para interagir com o erro mais recente.

mvndaai
fonte
1

Penso que a resposta de Benjamin Gruenbaum acima é a melhor solução para uma sequência lógica complexa, mas aqui está a minha alternativa para situações mais simples. Eu apenas uso uma errorEncounteredbandeira junto com return Promise.reject()para pular quaisquer declarações thenou catchdeclarações subseqüentes . Então ficaria assim:

let errorEncountered = false;
someCall({
  /* do stuff */
})
.catch({
  /* handle error from someCall*/
  errorEncountered = true;
  return Promise.reject();
})
.then({
  /* do other stuff */
  /* this is skipped if the preceding catch was triggered, due to Promise.reject */
})
.catch({
  if (errorEncountered) {
    return;
  }
  /* handle error from preceding then, if it was executed */
  /* if the preceding catch was executed, this is skipped due to the errorEncountered flag */
});

Se você tiver mais de dois pares then / catch, provavelmente deverá usar a solução de Benjamin Gruenbaum. Mas isso funciona para uma configuração simples.

Observe que a final catchtem apenas, return;e não return Promise.reject();, porque não há subseqüentes thenque precisamos pular, e isso contaria como uma rejeição de Promessa não tratada, da qual o Node não gosta. Como está escrito acima, a final catchretornará uma Promessa pacificamente resolvida.

temporary_user_name
fonte