ūüĆŅ Uma breve compara√ß√£o entre await e then para lidar com opera√ß√Ķes ass√≠ncronas

async + await vs then e legibilidade de código.


Em construção!

Existe uma conversa frequente nos f√≥runs de Node.js acerca de como lidar com fun√ß√Ķes ass√≠ncronas. Ainda que async e then nos fornecem a mesma funcionalidade para lidar com c√≥digo ass√≠ncrono em JavaScript, ambas s√£o distintas em seu funcionamento e efeitos colaterais.


Essa n√£o √© uma introdu√ß√£o as promises ou programa√ß√£o ass√≠ncrona, apenas devaneios sobre formas de lidar com o resultado dessas opera√ß√Ķes. Aqui est√£o excelentes materiais para aprender sobre recursos de programa√ß√£o ass√≠ncrona no JavaScript:


Trabalhando com Node.js (JavaScript) você provavelmente já se deparou com esses dois tipos de código:

  1. Requisição a API construída pelo time do then

fetch("https://emojihub.herokuapp.com/api/random")
    .then(response => response.json())
    .then(data => console.info(data))
  1. Requisição a API construída pelo time do async/await

const response = await fetch("https://emojihub.herokuapp.com/api/random")

const data = await response.json()

console.info(data)

Ambas resultam na mesma coisa: um emoji retornado randomicamente pela API e exibido no console. Mas cada API tem seus objetivos e seus respectivos casos de uso, e a sem√Ęntica de cada um √© diferente.

Defendo que não existe bala de prata e cada recurso tem sua razão de ser, então esse rascunho não serve para dizer qual é melhor, mas sim comparar ambos e ajudar na escolha, além de compartilhar minha preferência do ponto de vista da legibilidade.

História

Para que o pr√≥ximo t√≥pico fa√ßa sentido, vou come√ßar traduzindo cada uma das implementa√ß√Ķes:

  1. Utilizando then, estamos essencialmente:

'BUSCAR O EMOJI NA API'
    ENTÃO 'TRANSFORMAR A RESPOSTA EM JSON'
    ENTÃO 'IMPRIMIR OS DADOS'
  1. Utilizando await, estamos essencialmente:

RESPOSTA = (AGUARDE) 'BUSCAR O EMOJI NA API'

DADOS = (AGUARDE) 'TRANSFORMAR A RESPOSTA EM JSON'

'IMPRIMIR OS DADOS'

A implementação sozinha pode parecer pouca diferença, mas no segundo caso o código lê mais natural, muito semelhante ao síncrono. Enquanto o primeiro depende de bastante compreensão do https://subscription.packtpub.com/book/web-development/9781783287314/1/ch01lvl1sec10/the-callback-pattern e corre o risco do http://callbackhell.com/, caso seja mal implementado.

Para al√©m da naturalidade, cada formato tem objetivo diferente e internalidades diferentes. Apesar de parecer apenas um a√ß√ļcar sint√°tico, await implica em outras diferen√ßas tamb√©m.

E esse é um motivo histórico, em linha do tempo:

  1. Node.js foi criado profundamente atrelado ao Padrão Callback, que permitia a utilização de código assíncrono

  2. Foram criadas as promises e as novas APIs de then/catch/finally que permitiram minimizar o callback hell

  3. Finalmente foram criadas as fun√ß√Ķes ass√≠ncronas, que trouxeram o async/await e permitiram o c√≥digo mais leg√≠vel e natural (https://developers.google.com/web/fundamentals/primers/async-functions)

Apesar de n√£o serem sempre recursos concorrentes e terem seus pr√≥prios casos de uso, a nova sintaxe √© uma evolu√ß√£o levando em conta diversos dos problemas anteriores. Por isso, para alguns casos ela claramente ser√° mais compreens√≠vel, porque surgiu em um contexto diferente ‚ÄĒ de resolver problemas anteriores.

O mesmo vale para as novas APIs de manipula√ß√£o de cole√ß√Ķes ‚ÄĒ map, filter,reduce ‚ÄĒ nenhum deles substitui o bom e velho for ou ainda o while, apenas resolvem problemas espec√≠ficos.

Compreensibilidade

Escreverei pouco sobre este tópico porque o código fala mais do que mil palavras. E porque existem materiais melhores escritos sobre isso (como esse da Google https://developers.google.com/web/fundamentals/primers/async-functions ou esse da MDN https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Promises).

Naturalmente (ao menos na nossa região) fazemos a leitura de cima para baixo e da esquerda para a direita. E é isso que o código expressa:

console.log("#1");

await something();

console.log("#2");

console.log("#3");

Já utilizando then, não podemos garantir que o código a seguir será executado nessa ordem, mas sim que vai acontecer na ordem que for mais performática:

console.log("#1");

something().then(() => { 
    console.log("#3? (or #2)");
})

console.log("#2? (or #3)");

Para garantir a ordem, seria necess√°rio encadear a execu√ß√£o a resolu√ß√£o da Promise, causando aninhamento e poss√≠vel quebra de sem√Ęntica:

console.log("#1");

something().then(() => { 
    console.log("#2");
}).finally(() => {
    console.log("#3");
})

Código limpo é código limpo em qualquer lugar e com qualquer padrão, então sem aninhar seus thens. Em qualquer nível, sempre evite o callback hell: https://ibb.co/DkMWQfq.

Performance

Hoje em dia, o async/await √© mais r√°pido que as outras op√ß√Ķes, e muito mais r√°pido que implementa√ß√Ķes manuais de promise. Isso √© gra√ßas ao https://v8.dev/blog/fast-async, uma implementa√ß√£o da V8 que se aproveitou dos recursos para evitar o overhead que era causado pela promise extra no await .

De todo modo, sempre priorize realizar em ‚Äúparalelo‚ÄĚ processamentos que n√£o s√£o bloqueantes e dependentes entre si e aguardar por todos de uma vez s√≥. O promise.all √© um exemplo de recurso que possibilita isso, e pode ser utilizado em qualquer uma das maneiras. (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all)

Tratamento de Erros

Al√©m de possibilitar o tratamento de erros mais familiar (try/catch), a V8 trabalhou em um recurso poderos√≠ssimo para o tratamento de erros com async/await: Zero-cost async stack traces (https://docs.google.com/document/d/13Sy_kBIJGP0XT34V1CV3nkWya4TwYx9L3Yv45LdGB6Q/edit#heading=h.e6lcalo0cl47). Isso significa que o rastreamento de erro contempla as informa√ß√Ķes do c√≥digo s√≠ncrono e ass√≠ncrono, resolvendo uma dor forte do padr√£o de callbacks ou do then/catch, que era detectar a origem de erros n√£o capturados quando surgiam de c√≥digo ass√≠ncrono.

Por outro lado, como JavaScript não possui catchs condicionais, o tratamento específico de exceção pode ser mais verboso, enquanto temos o par then/catch que pode especificar por cada operação assíncrona.

Para se aprofundar nas conven√ß√Ķes de tratamento de erro, veja:

Afinal, qual forma é a melhor?

Nenhuma. Cada uma pode ter um caso de uso mais ou menos adequado, de acordo com o contexto.

Assim como não devemos encadear vários condicionais (if) também não deveríamos encadear vários resolvedores de promessas (then). E assim como não deveríamos implementar um complexo padrão de projeto para resolver uma validação de sim ou não, não deveríamos criar uma função para utilizar await quando o padrão de callback resolveria.

Por causa do ganho em compreensibilidade e das possibilidades de melhoria em performance que chegaram com a sintaxe async/await, a discussão ainda vem evoluindo para que await possa ser utilizado também em nível de módulos.

As propostas originais mencionadas servem como contexto para o problema do callback hell e as vantagens observadas com a nova sintaxe:

Isso não demonstra que ele seja melhor, mas que foi amplamente adotado e o uso vem crescendo. As pessoas olhariam estranho para um código cheio de then encadeado sendo que temos uma API muito mais limpa para lidar com isso.

E as pessoas tamb√©m olhariam estranho para uma lista de awaits dentro de uma mesma fun√ß√£o porque isso pode estar criando um bloqueio por for√ßar o comportamento s√≠ncrono, principalmente quando dentro de loops. (off-topic: Se voc√™ tem trabalhando com v√°rias opera√ß√Ķes encadeadas, recomendo fortemente o estudo de streams. Especialmente as pipelines https://nodejs.org/api/stream.html#streampipelinesource-transforms-destination-callback)

Lembrando do Fowler:

Any fool can write code that a computer can understand. Good programmers write code that humans can understand.

No fim, o melhor para a aplica√ß√£o √© n√£o bloquear o event loop. A forma como isso ser√° codificado depende muito mais do c√≥digo e das pessoas que v√£o ler ele ‚ÄĒ pessoalmente vejo que a sintaxe async/await costuma atender a maioria dos casos comuns, al√©m de ser compat√≠vel com a especifica√ß√£o de async iterators (https://tc39.es/proposal-async-iteration/).

Isso não exime de utilizar callbacks, eles são core do Node (https://nodejs.org/en/knowledge/getting-started/control-flow/what-are-callbacks/), ainda que eles próprios tratem async/await como a alternativa moderna para lidar com assincronismo (https://nodejs.dev/learn/modern-asynchronous-javascript-with-async-and-await).

Al√©m disso, vale mencionar que a V8, o motor que faz a magica acontecer e JS executar no Back-End com Node.js, aconselha (1) a utiliza√ß√£o de async/await em vez de promises escritas manualmente pelos ganhos em performance e (2) a utiliza√ß√£o das implementa√ß√Ķes nativas de promise em vez de bibliotecas pelos outros benef√≠cios mencionados anteriormente, na se√ß√£o de performance.

‚ÄúN√£o bloqueie o Event Loop‚ÄĚ

Afinal, por que tudo isso? (em homenagem ao meu amigo que xingou Node.js dizendo que ler um arquivo em Java na aula de POO foi mais simples que entender o Event Loop)

Quando algu√©m me questiona porque tantas APIs nativas do Node.js, ou mesmo as bibliotecas mais utilizadas, s√£o ass√≠ncronas e se n√£o seria mais simples elas simplesmente serem s√≠ncronas como em algumas outras linguagens, minha resposta gira em torno d√™: ‚ÄúN√£o bloqueie o Event Loop‚ÄĚ.

Node.js¬ģ √© descrito em sua pr√≥pria documenta√ß√£o como ‚Äúum ambiente de execu√ß√£o JavaScript ass√≠ncrono e orientado a eventos‚ÄĚ que utiliza um ‚Äúmodelo de I/O n√£o bloqueante‚ÄĚ. Na pr√°tica isso quer dizer muitas coisas, e a arquitetura do Node.js √© uma que vale estudar, mas podemos resumir em ‚Äún√£o bloqueie o event loop‚ÄĚ porque essa √© a estrat√©gia para que ele seja n√£o bloqueante e orientado a eventos por padr√£o.

O event loop nada mais é do que a thread principal, e devemos mantê-la livre de processos pesados para que ela se mantenha performática e segura. Em vez disso, enviamos tarefas pesadas para outras threads e lidamos com o resultado de forma assíncrona, através de eventos.

Por isso, para garantirmos nunca bloquear a thread principal, que tantas APIs nativas são assíncronas por padrão, e assim deve ser com as SDKs e bibliotecas que utilizamos. Se não é assíncrono por padrão, torne-a. Não bloqueie o Event Loop :)

Alguns recursos da própria documentação para se aprofundar nesse tema e na arquitetura do Node.js:


Conclus√£o

N√£o existe bala de prata, como tudo na tecnologia e na vida. Ambos s√£o recursos poderosos, assim como o pr√≥prio padr√£o callback e o ideal √© entender os dois para definir o mais adequado ao contexto ‚ÄĒ afinal foram criados em contextos diferentes para resolver problemas diferentes.

Quando estamos falando de tratamento de erros, legibilidade e desempenho, o async/await performa melhor, mas apenas isso não o torna a solução ideal.

Para se aprofundar nesse tema você pode ler mais nos materiais que eu utilizei como base para escrever:


Grat√≠ssima a voc√™ por ler at√© aqui. Espero que esse conte√ļdo tenha agregado de alguma forma. ūü§ó

Se quiser conversar sobre o tema você pode me enviar um e-mail para myreli@duck.com, deixar uma mensagem no Guestbook ou ainda um agradecimento.


***


ūüĆŅ Budding s√£o ideias que j√° revisei ou editei um pouco. Est√£o come√ßando a tomar forma, mas ainda precisam de refinamento. O que √© isso?