Rejeitar uma promessa apenas em casos de erro?


25

Digamos que eu tenho essa função de autenticação que retorna uma promessa. A promessa então se resolve com o resultado. Falso e verdadeiro são resultados esperados, a meu ver, e as rejeições devem ocorrer apenas em caso de erro. Ou, uma falha na autenticação é considerada algo que você rejeitaria uma promessa?


Se a autenticação falhar, você deve rejecte não deve retornar falso, mas se espera que o valor seja a Bool, teve êxito e deve resolver com o Bool, independentemente do valor. As promessas são uma espécie de proxies para valores - elas armazenam o valor retornado, portanto, somente se o valor não puder ser obtido, você deve reject. Caso contrário, você deveria resolve.

Essa é uma boa pergunta. Ele aborda uma das falhas do design da promessa. Existem dois tipos de erros, falhas esperadas, como quando um usuário fornece entrada incorreta (como falha no login) e falhas inesperadas, que são bugs no código. O design da promessa mescla os dois conceitos em um único fluxo, dificultando a distinção entre os dois.
ZzzzBov 02/12/16

1
Eu diria que resolver significa usar a resposta e continuar seu aplicativo, enquanto rejeitar significa cancelar a operação atual (e possivelmente tentar novamente ou fazer outra coisa).

4
outra maneira de pensar sobre isso - se essa fosse uma chamada de método síncrona, você trataria a falha de autenticação regular (nome de usuário / senha incorretos) como retornando falseou lançando uma exceção?
wrschneider

2
A API de busca é um bom exemplo disso. Ele sempre dispara thenquando o servidor responde - mesmo que um código de erro seja retornado - e você deve verificar o response.ok. O catchmanipulador é acionado apenas para erros inesperados .
precisa saber é o seguinte

Respostas:


22

Boa pergunta! Não há resposta difícil. Depende do que você considera ser excepcional em que ponto específico do fluxo .

Rejeitar a Promiseé o mesmo que criar uma exceção. Nem todos os resultados indesejados são excepcionais , o resultado de erros . Você poderia discutir seu caso de duas maneiras:

  1. Falha de autenticação deve rejecta Promise, porque o chamador está esperando um Userobjeto em troca, e qualquer outra coisa é uma exceção a esse fluxo.

  2. Falha de autenticação deve resolvea Promise, ainda que null, uma vez de fornecer as credenciais erradas não é realmente um excepcional caso, e o chamador não deve esperar o fluxo para sempre resultam em um User.

Observe que estou analisando o problema do lado do chamador . No fluxo de informações, o chamador espera que suas ações resultem em um User(e qualquer outra coisa é um erro) ou faz sentido que esse chamador em particular lide com outros resultados?

Em um sistema de várias camadas, a resposta pode mudar à medida que os dados fluem pelas camadas. Por exemplo:

  • A camada HTTP diz RESOLVE! A solicitação foi enviada, o soquete fechado corretamente e o servidor emitiu uma resposta válida. A API de busca faz isso.
  • A camada de protocolo diz REJEITAR! O código de status na resposta foi 401, o que é bom para HTTP, mas não para o protocolo!
  • A camada de autenticação diz NÃO, RESOLVE! Ele captura o erro, já que 401 é o status esperado para uma senha incorreta e é resolvido para um nullusuário.
  • O controlador de interface diz NENHUM ASSIM, REJEITE! A exibição modal na tela esperava um nome de usuário e um avatar, e qualquer coisa que não seja essa informação é um erro neste momento.

Este exemplo de 4 pontos é obviamente complicado, mas ilustra 2 pontos:

  1. Se algo é uma exceção / rejeição ou não, depende do fluxo circundante e das expectativas
  2. Diferentes camadas do seu programa podem tratar o mesmo resultado de maneira diferente, pois estão localizadas em diferentes estágios do fluxo

Então, novamente, nenhuma resposta difícil. Hora de pensar e projetar!


6

Portanto, o Promises tem uma boa propriedade que eles trazem JS de linguagens funcionais, ou seja, eles realmente implementam esse Eitherconstrutor de tipos que cola dois outros tipos, o Lefttipo e o Righttipo, forçando a lógica a assumir uma ramificação ou outra ramo.

data Either x y = Left x | Right y

Agora você está percebendo que o tipo do lado esquerdo é ambíguo para promessas; você pode rejeitar com qualquer coisa. Isso ocorre porque o JS é fracamente digitado, mas você deve ser cauteloso se estiver programando defensivamente.

O motivo é que o JS também recebe throwinstruções do código de tratamento de promessas e o agrupa no Leftlado dele. Tecnicamente, em JS, você pode de throwtudo, incluindo verdadeiro / falso, uma sequência ou um número: mas o código JavaScript também lança coisas semthrow (quando você faz coisas como tentar acessar propriedades em valores nulos) e existe uma API estabelecida para isso (o Errorobjeto) . Portanto, quando você tenta capturar, geralmente é bom poder assumir que esses erros são Errorobjetos. E como a rejectpromessa de aglomeração de erros de qualquer um dos erros acima, geralmente você deseja apenas throwoutros erros, para fazer com que sua catchdeclaração tenha uma lógica simples e consistente.

Portanto, embora você possa colocar um if-condicional no seu catche procurar por erros falsos, nesse caso, o caso da verdade é trivial,

Either (Either Error ()) ()

você provavelmente preferirá a estrutura lógica, pelo menos para o que sai imediatamente do autenticador, de um booleano mais simples:

Either Error Bool

De fato, o próximo nível de lógica de autenticação é provavelmente retornar algum tipo de Userobjeto que contém o usuário autenticado, para que isso se torne:

Either Error (Maybe User)

e isso é mais ou menos o que eu esperaria: retornar nullno caso em que o usuário não estiver definido, caso contrário, retorne {user_id: <number>, permission_to_launch_missiles: <boolean>}. Eu esperaria que o caso geral de não estar logado seja recuperável, por exemplo, se estivermos em algum tipo de modo "demo para novos clientes" e não deva ser misturado com bugs nos quais chamei acidentalmente object.doStuff()quando object.doStuffestava undefined.

Agora com o que disse, o que você pode querer fazer é definir um NotLoggedInou PermissionErrorexceção que deriva Error. Então, nas coisas que realmente precisam, você deseja escrever:

function launchMissiles() {
    function actuallyLaunchThem() {
        // stub
    }
    return getAuth().then(auth => {
        if (auth === null) {
            throw new PermissionError('Cannot launch missiles without permission, cannot have permission if not logged in.');
        } else if (auth.permission_to_launch_missiles) {
            return actuallyLaunchThem();
        } else {
            throw new PermissionError(`User ${auth.user_id} does not have permission to launch the missiles.`);
        }
    });
}

3

Erros

Vamos falar sobre erros.

Existem dois tipos de erros:

  • erros esperados
  • erros inesperados
  • erros de um por um

Erros esperados

Os erros esperados são estados em que a coisa errada acontece, mas você sabe que pode, então lida com isso.

São coisas como entrada do usuário ou solicitações do servidor. Você sabe que o usuário pode cometer um erro ou que o servidor pode estar inoperante; portanto, escreva algum código de verificação para garantir que o programa solicite a entrada novamente ou exiba uma mensagem ou qualquer outro comportamento apropriado.

Estes são recuperáveis ​​quando manuseados. Se deixados sem tratamento, eles se tornam erros inesperados.

Erros inesperados

Erros inesperados (bugs) são estados em que a coisa errada acontece porque o código está errado. Você sabe que eles acabarão por acontecer, mas não há como saber onde ou como lidar com eles, porque, por definição, são inesperados.

São coisas como erros de sintaxe e de lógica. Você pode ter um erro de digitação no seu código, pode ter chamado uma função com os parâmetros incorretos. Normalmente não são recuperáveis.

try..catch

Vamos conversar try..catch.

Em JavaScript, thrownão é comumente usado. Se você procurar exemplos em código, eles serão poucos e distantes entre si, e geralmente estruturados de acordo com as linhas de

function example(param) {
  if (!Array.isArray(param) {
    throw new TypeError('"param" should be an array!');
  }
  ...
}

Por esse try..catchmotivo , os blocos também não são tão comuns no fluxo de controle. Geralmente, é muito fácil adicionar algumas verificações antes de chamar métodos para evitar erros esperados.

Os ambientes JavaScript também são bastante indulgentes; portanto, erros inesperados também costumam ser detectados.

try..catchnão precisa ser incomum. Existem alguns casos de uso interessantes, que são mais comuns em linguagens como Java e C #. Java e C # têm a vantagem de catchconstruções digitadas , para que você possa diferenciar entre erros esperados e inesperados:

C # :
try
{
  var example = DoSomething();
}
catch (ExpectedException e)
{
  DoSomethingElse(e);
}

Este exemplo permite que outras exceções inesperadas fluam e sejam tratadas em outro lugar (como sendo registrado e fechando o programa).

Em JavaScript, essa construção pode ser replicada via:

try {
  let example = doSomething();
} catch (e) {
  if (e instanceOf ExpectedError) {
    DoSomethingElse(e);
  } else {
    throw e;
  }
}

Não é tão elegante, o que é parte da razão pela qual é incomum.

Funções

Vamos falar sobre funções.

Se você usar o princípio da responsabilidade única , cada classe e função deve servir a um propósito singular.

Por exemplo, authenticate()pode autenticar um usuário.

Isso pode ser escrito como:

const user = authenticate();
if (user == null) {
  // keep doing stuff
} else {
  // handle expected error
}

Como alternativa, pode ser escrito como:

try {
  const user = authenticate();
  // keep doing stuff
} catch (e) {
  if (e instanceOf AuthenticationError) {
    // handle expected error
  } else {
    throw e;
  }
}

Ambos são aceitáveis.

Promessas

Vamos falar de promessas.

Promessas são uma forma assíncrona de try..catch. Chamando new Promiseou Promise.resolveinicia seu trycódigo. Ligar throwou Promise.rejectenviar você para o catchcódigo.

Promise.resolve(value)   // try
  .then(doSomething)     // try
  .then(doSomethingElse) // try
  .catch(handleError)    // catch

Se você possui uma função assíncrona para autenticar um usuário, pode escrevê-la como:

authenticate()
  .then((user) => {
    if (user == null) {
      // keep doing stuff
    } else {
      // handle expected error
    }
  });

Como alternativa, pode ser escrito como:

authenticate()
  .then((user) => {
    // keep doing stuff
  })
  .catch((e) => {
    if (e instanceOf AuthenticationError) {
      // handle expected error
    } else {
      throw e;
    }
  });

Ambos são aceitáveis.

Aninhamento

Vamos falar sobre aninhamento.

try..catchpode ser aninhado. Seu authenticate()método pode ter internamente um try..catchbloco como:

try {
  const credentials = requestCredentialsFromUser();
  const user = getUserFromServer(credentials);
} catch (e) {
  if (e instanceOf CredentialsError) {
    // handle failure to request credentials
  } else if (e instanceOf ServerError) {
    // handle failure to get data from server
  } else {
    throw e; // no idea what happened
  }
}

Da mesma forma, as promessas podem ser aninhadas. Seu authenticate()método assíncrono pode usar internamente promessas:

requestCredentialsFromUser()
  .then(getUserFromServer)
  .catch((e) => {
    if (e instanceOf CredentialsError) {
      // handle failure to request credentials
    } else if (e instanceOf ServerError) {
      // handle failure to get data from server
    } else {
      throw e; // no idea what happened
    }
  });

Então qual é a resposta?

Ok, acho que é hora de eu realmente responder à pergunta:

Uma falha na autenticação é considerada algo que você rejeitaria uma promessa?

A resposta mais simples que posso dar é que você deve rejeitar uma promessa em qualquer lugar que desejaria throwuma exceção, se fosse um código síncrono.

Se seu fluxo de controle for mais simples, com algumas ifverificações em suas thendeclarações, não há necessidade de rejeitar uma promessa.

Se o seu fluxo de controle for mais simples, rejeitando uma promessa e, em seguida, verificando tipos de erros no seu código de tratamento de erros, faça-o.


0

Eu usei o ramo "rejeitar" de uma promessa para representar a ação "cancelar" das caixas de diálogo jQuery UI. Parecia mais natural do que usar o ramo "resolver", principalmente porque geralmente há várias opções de "fechamento" em uma caixa de diálogo.


A maioria dos puristas que eu conheço discordaria de você.

0

Lidar com uma promessa é mais ou menos como a condição "se". Cabe a você decidir se deseja "resolver" ou "rejeitar" se a autenticação falhar.


1
promessa é assíncrona try..catch, não if.
ZzzzBov 02/12/16

@zzzBox, de acordo com essa lógica, você deve usar uma promessa como assíncrona try...catche simplesmente dizer que, se conseguir concluir e obter um resultado, deverá resolver independentemente do valor recebido, caso contrário você deve rejeitar?

@Algo aqui, não, você interpretou mal o meu argumento. try { if (!doSomething()) throw whatever; doSomethingElse() } catch { ... }é perfeitamente bom, mas a construção que a Promiserepresenta é a try..catchparte, não a ifparte.
ZzzzBov 02/12/16

@zzzzBov Eu tenho isso com toda a justiça :) Eu gosto da analogia. Mas minha lógica é simplesmente que, se doSomething()falhar, será lançada, mas, se não, poderá conter o valor que você precisa (o que você disse ifacima é um pouco confuso, pois não faz parte da sua ideia aqui :)). Você só deve rejeitar se houver um motivo para lançar (na analogia), portanto, se o teste falhar. Se o teste for bem-sucedido, você sempre deve resolver, independentemente de seu valor ser positivo, certo?

@Algo aqui, eu decidi escrever uma resposta (assumindo que isso permaneça aberto o tempo suficiente), porque comentários não são suficientes para expressar meus pensamentos.
ZzzzBov 02/12/16
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.