Desde o lançamento da es6, muitos recursos novos chegaram ao NodeJS, mas nenhum teve o mesmo impacto que as promises. As promessas foram desenvolvidas para o navegador antes mesmo da existência da es6. Havia várias implementações que foram usadas, como o objeto diferido do jQuery, antes que o padrão as tornasse obsoletas. As promessas eram bastante úteis no cliente, especialmente se o usuário tivesse que fazer muitas chamadas assíncronas ou se a API fosse uma bagunça completa e o usuário tivesse que reunir as chamadas assíncronas de todos os lugares. Para mim, o último caso era geralmente o caso ou, pelo menos, foi quando achei as promessas mais úteis. A capacidade de transmitir qualquer promessa e anexar tantos retornos de chamada a ela, bem como encadeá-los quantas vezes o senhor quisesse, tornava as promessas altamente versáteis, mas isso era para o cliente. O servidor é diferente. No servidor, o senhor precisa fazer uma quantidade absurda de chamadas assíncronas em comparação com o cliente. Normalmente, o cliente só precisa chamar o servidor de API de forma assíncrona, mas o servidor precisa se comunicar com o banco de dados, o sistema de arquivos, as APIs externas, como pagamento e comunicação, e qualquer serviço principal que o senhor precise usar. Essencialmente: um monte de coisas. Quaisquer problemas que possamos ter no cliente devido a promessas serão ampliados no servidor devido à maior taxa de uso e à maior chance de cometer erros.

Se observarmos o código que usamos para fazer promessas, a princípio, elas não parecem muito diferentes das funções normais, mas há um recurso fundamental que as torna únicas. As promessas capturam todas as exceções que são levantadas dentro delas de forma síncrona. Isso, embora seja muito útil na maioria dos casos, pode causar alguns problemas se o senhor não estiver preparado para lidar com elas. Quando uma exceção é lançada, a promessa é rejeitada e chamará seu callback rejeitado, se houver algum. Mas o que acontece se não lidarmos com o estado rejeitado da promessa? Depende da versão do NodeJS, mas geralmente um aviso será impresso e a função que gerou a exceção será encerrada. A rejeição de promessas por meio do lançamento de exceções é algo que era usado com frequência nos antigos navegadores das bibliotecas de promessas e é considerado normal, mas será que isso é realmente uma coisa boa? É bom ou, pelo menos, não há problema se o senhor realmente quiser rejeitar uma promessa, mas e se lançar um erro não porque queria, mas porque cometeu um erro? Nesse caso, o senhor precisa encontrar o erro e corrigi-lo, e é nesse caso específico que deixar uma exceção travar o servidor e imprimir um rastreamento de pilha seria realmente útil. Então, o que temos em vez disso? No NodeJS 6 e 7, receberemos um UnhandledPromiseRejectionWarning que, na maioria dos casos, dirá ao senhor o que causou o erro, mas não onde. No nó 8, também receberemos um breve rastreamento de pilha. Portanto, a atualização para o nó 8 pode resolver nossos problemas, portanto, desde que o senhor possa fazer isso, pode pensar que é tudo o que precisamos fazer para resolver esse problema. Infelizmente, o nó 8 ainda não é usado pela maioria das empresas e representa menos de 10% do mercado.



Desde o nó 7, um aviso de rejeição de promessa também dará ao senhor outro aviso:

“DeprecationWarning: As rejeições de promessa não tratadas estão obsoletas. No futuro, as rejeições de promessa que não forem tratadas encerrarão o processo do Node.js com um código de saída diferente de zero.”

Observe que esse aviso não diz que ele gerará uma exceção, mas que o servidor será derrubado, não importa o que aconteça. Isso é bastante severo, o senhor não acha? Essa alteração definitivamente quebraria alguns códigos se fosse implementada hoje. O interesse em UnhandledPromiseRejectionWarning aumentou em conjunto com a popularidade e o uso de promises. Podemos até medir o quanto usando o Google Trends.



As pessoas que pesquisaram esse aviso específico aumentaram significativamente desde que as promessas nativas e esse aviso foram introduzidos no node. Em 2017, o número de pesquisas dobrou, o que provavelmente significa que o número de pessoas que usam promises no NodeJS também dobrou. Talvez esse seja o motivo pelo qual a equipe do node queira eliminar completamente o aviso de sua pilha.

É compreensível que, caso uma rejeição de promessa não seja tratada, seja melhor travar o servidor do que apenas emitir um aviso. Imagine o que aconteceria com uma rota de API se uma rejeição não fosse tratada. Nesse caso, a resposta não seria enviada ao cliente, pois a função seria encerrada antes de chegar a esse ponto, mas também não fecharia o soquete, pois o servidor não travaria, e apenas esperaria até obter o tempo limite após dois minutos. Se várias solicitações desse tipo fossem feitas ao servidor no período de dois minutos, poderíamos ficar sem soquetes muito rapidamente, o que bloquearia nosso serviço para sempre. Se, por outro lado, falharmos e reiniciarmos, poderemos atender a algumas solicitações por algum tempo, pelo menos. Claramente, nenhum dos casos é desejável, portanto, devemos colocar um catch no final de cada cadeia de promessas que criamos. Isso evitaria que o servidor falhasse ou emitisse um aviso, o que também nos permitiria responder às solicitações de API de alguma forma. O problema com o catch é que ele é apenas um callback de rejeição glorificado, não diferente dos fornecidos por meio do segundo parâmetro do método then promise method.

O maior problema que tenho com as promessas é que todas as exceções são capturadas pelo manipulador de rejeição, independentemente do motivo pelo qual foram levantadas. É normal que as chamadas assíncronas possam falhar e é normal lidar com essa possibilidade, mas a captura de todas as exceções também captura os erros em seu código. Quando normalmente o sistema falharia e apresentaria um rastreamento de pilha com promessas, o código tentará lidar com a exceção e possivelmente falhará na chamada assíncrona silenciosamente, permitindo que o restante do código seja executado sem interrupções. É muito difícil diferenciar a rejeição de uma promessa lançada pelo sistema de uma exceção lançada pelo código e, mesmo que o senhor pudesse fazer isso, seria apenas excesso de engenharia. A única maneira de lidar adequadamente com as promessas é escrever um grande número de testes, mas o fato de que o senhor simplesmente precisa fazer isso não é um recurso positivo em si. Nem todo mundo faz isso e nem todo mundo tem permissão para fazer isso, e não há nenhuma boa razão para dificultar as coisas para eles.

As exceções geradas em qualquer chamada assíncrona não podem ser capturadas por um bloco try catch, portanto, faz sentido capturá-las se necessário. A palavra-chave aqui é “necessário”. Não é necessário capturá-las durante o desenvolvimento, assim como o expressJS não as capturará, exceto em produção, mas mesmo que o expressJS as capture, no mínimo interromperá a execução do código para essa chamada específica, o que não é possível fazer com as promises. A maneira correta de tratar exceções em promessas ou em qualquer outra chamada assíncrona é (a) fornecer a elas um manipulador de exceções que, se fornecido, será executado se uma exceção for lançada e (b) interromper a execução da cadeia de promessas ou do restante do código. Esse manipulador pode ser propagado ao longo da cadeia de promessas e, se não for definido, permitirá que a exceção se acumule e trave o servidor.

Algumas pessoas acham que é necessário lançar promessas internas para invocar o callback de rejeição, mas isso nunca foi verdade. Mesmo hoje, o senhor pode simplesmente retornar um Promise.reject(someError) para falhar em qualquer promessa em que o senhor normalmente faria um throw. Se o senhor perguntasse por que os erros de lançamento são usados para rejeitar promessas, poucos saberiam responder. Para começar, não sei se há uma resposta, a não ser o fato de que essa foi a forma como as promises foram implementadas no navegador há muitos anos, e a ECMA simplesmente reimplementou esse padrão um tanto quebrado no ES6 e o Node o adotou a partir daí. Foi uma boa ideia introduzir essa versão de promessas no padrão e migrá-la para o lado do servidor? O fato de o Node estar se afastando do padrão deve nos dar algumas dúvidas. Nem mesmo é verdade que as promessas são a única maneira de lidar com o temido inferno do callback. Existem outras soluções, como o async e RQ por exemplo, que incluem métodos como parallel e waterfall que permitem que os programadores executem chamadas assíncronas de forma mais organizada. Pelo menos no lado do servidor, é muito raro precisar de mais do que uma combinação dos métodos fornecidos por essas bibliotecas. O motivo pelo qual as promessas foram introduzidas no padrão pode ter sido simplesmente porque elas eram populares graças ao jQuery. Implementar o tratamento de exceções seria mais fácil com uma biblioteca assíncrona tradicional, mas isso não significa que não possa ser feito com promessas. Mesmo hoje em dia, o senhor poderia substituir a função then no protótipo da Promise e no construtor da Promise para fazer isso.




Promise.prototype.then = (function () {
  const then = Promise.prototype.then;
  const fixCall = function(promise, next){
    if (!next) {
      return null;
    }
    return function (val) {
      try {
        let newPromise = next.call(promise, val);
        if(newPromise){
          newPromise.error = promise.error;
        }
        return newPromise;
      } catch (exception) {
        setTimeout(function () {
          if (promise.error) {
            promise.error(exception);
          } else {
            throw(exception);
          }
        }, 0);
        return new Promise(()=>{});
      }
    }
  };
  return function (success, fail, error) {
    this.error = this.error || error;
    let promise = then.call(this, fixCall(this, success), fixCall(this, fail));
    promise.error = this.error;
    return promise;
  }
}());
function createPromise(init, error){
  let promise = new Promise(init);
  promise.error = error;
  return promise;
}  



Mencionei anteriormente que as chamadas assíncronas não podem ser capturadas por um bloco try catch e isso é verdade mesmo dentro de uma promessa, portanto, é possível sair de uma promessa usando um setTimeout ou um setImmediate call. Portanto, se capturarmos uma exceção, faremos isso, a menos que um manipulador de exceções tenha sido fornecido, caso em que o chamaremos. Em ambos os casos, queremos interromper a execução do restante da cadeia de promessas e podemos fazer isso simplesmente retornando uma promessa vazia que nunca é resolvida. Obviamente, esse código está aqui apenas para demonstrar que isso pode ser feito e, embora agora o senhor possa tratar exceções adequadamente, não perdeu nenhuma das funcionalidades originais.

Um grande problema das promessas é que o senhor pode estar usando-as sem perceber. Existem algumas bibliotecas populares que usam promessas nos bastidores e, ao mesmo tempo, permitem que o senhor especifique retornos de chamada tradicionais, mas os executará dentro das promessas que elas usam. Isso significa que qualquer exceção será capturada sem que o senhor saiba ou possa adicionar um reject para elas, de modo que, por enquanto, elas gerarão o aviso UnhandledPromiseRejectionWarning. O senhor certamente coçará a cabeça se vir esse aviso sem ter uma única promessa em seu código, da mesma forma que eu fiz há algum tempo. Normalmente, o senhor receberia uma mensagem de erro relativamente útil no aviso, mas se estiver executando o código incorreto dentro de um método de uma biblioteca assíncrona, ele provavelmente falhará de uma forma que a maioria de nós não consegue compreender. Depois de inserir uma promessa, todos os seus retornos de chamada serão executados no contexto dessa promessa e, a menos que o usuário saia dela usando algo como setTimeout ela assumirá o controle de todo o seu código sem que o senhor perceba. Colocarei aqui um exemplo que usa uma versão mais antiga do módulo Monk MongoDB. Esse bug foi corrigido, mas nunca se pode saber se outra biblioteca fará algo semelhante. Então, sabendo que o Monk usa promessas, o que o senhor acha que acontecerá se eu executar esse código em um banco de dados vazio?



async.parallel({
  value: cb => collection.find({}, cb)
}, function (err, result) {
  console.log(result.test.test); //this line throws an exception because result is an empty object
});



A resposta é:

(node:29332) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Callback was already called.


A menos que o senhor esteja usando o Node 8, nesse caso, obterá:



(node:46955) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:46955) UnhandledPromiseRejectionWarning: Error: Callback was already called.
    at /node_modules/async/dist/async.js:955:32
    at /node_modules/async/dist/async.js:3871:13
    at /node_modules/monk-middleware-handle-callback/index.js:13:7
    at <anonymous>
at process._tickCallback (internal/process/next_tick.js:188:7)


Boa sorte para encontrar a causa disso 😊.


Fontes:


  1. https://semaphoreci.com/blog/2017/11/22/nodejs-versions-used-in-commercial-projects-in-2017.html
  2. https://trends.google.com/trends/explore?date=2016-03-30%202018-03-30&q=UnhandledPromiseRejectionWarning
  3. https://github.com/nekdolan/promise-tests

Daniel Boros

Sobre Daniel Boros

Sou um desenvolvedor web remoto full stack e entusiasta de Javascript e NodeJS. Tenho experiência na criação de aplicativos híbridos, servidores api nodeJS para o backend e VueJS para aplicativos baseados na Web. Trabalhei em todo o mundo para várias empresas de pequeno e grande porte, no local e também remotamente. Adquiri experiência nos setores de marketing e apostas on-line em geral.