Async / Await e Promises – Qual é o melhor? Quando usar?

Eu sempre me fiz essa pergunta e já vi muita gente se fazer também. No final das contas, um é melhor que o outro? Existe uma maneira de definir quando usar Async/Await no Javascript e quando usar os chains da promise?


Como todo assunto no Front-end hoje, este é um pouco polêmico. Isso porque sempre temos dois extremos, o programador que não vê valor nenhum numa determinada tecnologia e aquele que é apaixonado e usa sem fazer a menor idéia do por quê.

Ambos os perfis são igualmente destrutivos, mas vamos lá, eu vou tentar deixar aqui a minha idéia de quando usar um e quando usar o outro. Mas primeiro, vou partir do princípio que você pode não saber sobre eles, e minha idéia aqui é abstrair esses caras ao máximo, não vou tentar dar uma explicação acadêmica, vou tentar dar uma visão mais simplificada sobre as duas.


O que são “Promises” no Javascript ? Quando usá-las?

Não espere aqui a explicação padrão que existe por aí. Não vou falar sobre os benefícios e o quão maravilhoso é versus callbacks, isso porque já tem muito post fazendo esse tipo de comparação e porque eu não sou muito fã desse tipo de explicação porque de forma indireta, te faz acreditar que callbacks são ruins e que existe algo melhor para substituí-los.

Isso não é nem um pouco verdade, tanto é que as promises se beneficiam desta técnica, que é você passar uma função como argumento para outra função, para que internamente esta outra função possa chamar a sua no momento correto, este é o conceito básico dos callbacks é a forma mais simples que eu consigo explicar o que são.

Promises são wrappers imediatamente retornados para você como um objeto ao qual você pode previamente definir o que você gostaria de fazer, após ela se resolver.

const userName = axios.get('/meu-servico/users/name/1')
    .then(({ data }) => data )
    .then( name => name.toUpperCase() )

Ou seja, userName é o seu wrapper, sua caixinha, que possui algumas características interessantes, como o método .then e outros que não vale a pena falar sobre agora. Mas este é um dos métodos mais interessantes e usados, porque ele tem duas caraterísticas que eu acredito serem muito úteis para a qualidade do seu código.

  1. Te permite encadear varias funções de callback que você passa, onde essas funções recebem o valor retornado do .then anterior.
  2. Automágicamente “desenvelopa” uma promise caso você à retorne em algum .then.

Bom, de forma rasa essas são as propriedades que mais me impactam no dia a dia no uso de promises, no código de exemplo o que eu to fazendo é usar uma biblioteca conhecida como o axios , que retorna uma promise após uma chamada de função de request. Porém o valor da promise tem um formato ( shape ) específico que o axios gera, onde ele coloca o valor da requisição, numa propriedade data.

Então no exemplo que eu dei, o que eu to fazendo é uma série de transformações, onde, eu pego o valor do data e passo pra próxima função, (já que estamos usando arrow functions, não precisamos declarar o return). Logo em seguida eu pego esse valor, que no caso fictício é uma String e então eu transformo-a para uma string com todos os caracteres em maiúsculo.

Isso é apenas um exemplo, mas na vida real, se você abstrai as suas aplicações usando uma camada de services ( Serviços ) onde você possui uma coleção de funções que vão fazer requisições e transformações, você provavelmente vai usar e deve se beneficiar dessa característica das Promises.

As funções que você passa para os métodos .then das promises são callbacks, então o argumento de que promises são melhores ou substitutos de callbacks não fazem sentido nem para o próprio funcionamento das mesmas, e existem ocasiões em que não faz sentido usar promises e sim callbacks, um exemplo simples:

const button = document.querySelector('#main-button')
const mycallback = () => console.log('hello!')

button.addEventListener('click', mycallback)

Não faz sentido nenhum usar promises nessa situação, nesse caso, a melhor e única saída é callback. Outro exemplo prático são os callbacks que você utiliza nas aplicações server-side usando node, quando registra uma função nos métodos de request com o express, por exemplo: app.get('/', homeController).

Mas porque é importante então se não é uma substituição para os callbacks?

Uma das características mais interessantes que eu acho desse pattern é que você pode definir uma série de transformações assim que você faz a sua chamada assíncrona e logo em seguida, retornar e passar para outra função ou componente que por sua vez pode adicionar novas funções para transformar novamente os dados que recebe.

Isso tem um potencial de deixar seu código super desacoplado e fácil de dar manutenção se você utilizar sua criatividade. Por exemplo, você pode utilizar as Promises como alternativa nativa para implementação do Design Pattern Decorator. Genial né? Escreve no comentário se você gostaria de um post explicando formas de como fazer isso ;).


O que são os Async/Await e quando usar então?

O Async/Await é uma forma de escrever o código um pouco mais simplificado, sem que precise usar os encadeamentos .then. “Ué… mas você não vai falar que Async/Await é uma forma do código parecer síncrono como todo mundo fala?” Não.

Embora esta seja uma consequência, para mim na prática, é apenas um syntax sugar, para que você possa simplificar a leitura do seu código, dependendo do lugar onde você usa promises, os encadeamentos do then podem deixar o código verboso desnecessariamente.

Outra coisa, não acredito que sirva para você se livrar de pensar de forma assíncrona, você ainda precisa saber que o código não vai ser executado na hora que se espera, e também por isso que você precisa inclusive deixar explícito isso para quem lê seu código, usando as palavras-chaves : async e await.

Então, partindo do princípio que você modulariza seu projeto, e você tem uma camada de serviços que já faz todas as transformações necessárias e te retornam apenas uma promise como resultado. Usando o exemplo anterior do userName, imagine que aquele trecho de código já está em uma camada de serviços abstraída e exporta uma função que te retorna aquela promise com a string tratada em uppercase, chamada getUserName().

Imaginando que você está no seu componente e este importa ou recebe via Injeção de Dependências esse serviço, nós sabemos que a camada já resolveu o problema de transformar os dados e nesta situação você apenas precisa pegar o dado e repassar para outra tarefa.

Então no seu componente você tem uma função como essa:

const formUser = document.querySelector('form[name=user]')

const onSubmit = async (e) => {
    e.preventDefault()
    const id = e.target.userId.value 
    const name = await getUserName( `/meu-servico/users/name/${id}` )
    facaOutraCoisaAgora( name )
}

formUser.addEventListener('submit', onSubmit)

O código acima representa um trecho de um componente ao qual possui um formulário e no submit ele pega o valor de um campo com name userId que pode ser algum input[hidden] por exemplo, manda pro serviço, resgata o nome e depois faz outra coisa chamando uma outra função.

No trecho acima dá pra ver que não é o momento de fazer transformações, nós já delegamos isso para outra camada (Serviços). Dessa forma seu código fica um pouco mais “straightforward” ou seja, mais direto ao ponto.

A partir daí, você já pode começar a pensar em quais momentos do código você prefere usar um ou outro, sempre colocando como prioridade a legibilidade para que o entendimento do que está fazendo fique sempre claro e simples.

Quanto mais verboso seu código for, quanto mais emaranhado ele estiver com lógicas que resolvem múltiplos problemas ao mesmo tempo, pior será para entendê-lo depois, portanto a organização do seu código é de sua responsabilidade e esta estratégia é uma sugestão para utilizar essa “ferramenta” para simplificar o seu código.

É possível em algumas situações que você mesmo assim precise de transformações, é quando é hora de usar o método then das promises.

As keywords async/await vão transformar sua função em uma função que retorna sempre uma promise. A função do exemplo acima é uma função com side-effect para resolver tasks relacionados à interação com o DOM, não temos return, porém se tivéssemos retornado, e chamado essa função manualmente, receberíamos uma promise e apenas conseguiríamos o valor dela usando os .then ou se esta função fosse definida como assíncrona e usasse internamente a keyword await.

Minha conclusão simplista

O Async/Await nada mais é do que uma versão alternativa para “desenvelopar” o conteúdo de uma promise.

É claro que o assunto é muito mais profundo do que isso, mas aí existe uma série de artigos, cursos e vídeos sobre o assunto que te introduzirão detalhes importantes e suas consequências.

Há situações que deve se preocupar, principalmente com relação à tratamento de erros e paralelismo, mas isso fica para uma leitura posterior, e no final você vai acabar aprendendo na prática. O foco aqui é mais filosófico, partindo do princípio que já os utiliza de forma consistente e já possui uma certa experiência ao utilizá-los nos projetos que trabalha e agora, se vendo na situação de poder escolher, saber qual regra irá seguir ou se vai simplesmente usar o “feeling”.

É muito difícil ter que definir quando usar um ou outro porque os cenários são inúmeros, porém aprendi com uma professora lá na época da escola, quando ainda era muito pequeno para entender os desafios do mundo, que é sempre bom ter uma regra do dedão, “…ela serve para ilustrar como tomamos decisões usando estratégias simplificadoras, sem a análise racional detalhada nos métodos prescritivos. Essas regras dos dedões são chamadas de heurísticas.”

Já existem muitas situações que nós programadores precisamos tomar decisões, nós sabemos muito bem com a experiência que não somos muito bons nisso não é mesmo? Então vale sempre a pena se preocupar com aquelas decisões que são mais custosas e/ou importantes para gente, ou quando nos depararmos nas excessões, situações onde a regra do dedão não foi ótima, então nesse caso faz sentido gastar energia para pensar em uma alternativa.

Um abraço!