JavaScript assíncrono: callbacks, promises e async functions

Alcides Queiroz
8 min readMar 5, 2018

--

É sempre melhor pedir uma pizza assincronamente…

Para fins didáticos, vejamos dois exemplos do mundo real:

Exemplo 1) Eu vou a uma pizzaria, peço uma pizza para viagem no balcão e fico plantado lá, só esperando me entregarem o pedido para que eu possa ir embora.

Exemplo 2) Eu vou a uma pizzaria, peço uma pizza para viagem no balcão e, enquanto ela não fica pronta, dou uma passada na livraria ao lado para folhear a biografia de algum vlogger teen e tentar descobrir uma serventia para aquela obra se não como alternativa emergencial ao papel higiênico.

Se você conhece os conceitos básicos de assincronismo, é fácil compreender que o exemplo 1 é um caso típico de operação síncrona, pois minha vida parou completamente até que o pizzaiolo completasse seu trabalho. No exemplo 2, por outro lado, resolvi aproveitar meu precioso tempo ocioso degustando alguma pérola literária repleta de vazio. Isso é assíncrono… e uma grande burrice também.

Como essa odisseia começou?

TL;DR desta seção: A popularização do Ajax causou a explosão do assincronismo na web.

Nos bizarros primeiros anos deste milênio, operações assíncronas não eram tão comuns na web. Um reflexo disso era que a maior parte da lógica de uma aplicação se encontrava no seu backend. Uma tela de cadastro na web, por exemplo, normalmente consistia em um formulário comum, fazendo POST nativo do HTML para alguma url mágica que “daria conta do trabalho”, incluindo fazer todas as validações necessárias. REST/JSON?? Nope. Provavelmente um spaghetti delicioso em PHP, ASP clássico ou algo semelhante. O usuário clicava num botão e a página fazia um round-trip no servidor, reaparecendo em seguida.

Alguns desbravadores (incluo-me nesse time, com certo orgulho), em busca de uma experiência mais decente para o usuário final, usavam técnicas diversas para conseguir enviar dados para o servidor sem que a página atual fosse recarregada. Geralmente a gambiarra consistia em ocultar algo que não havia sido projetado para esse tipo de uso: Iframes ocultos, applets Java ocultos, flashs ocultos… já dá para notar que o negócio não era bonito.

Até que em 2005, Jesse James Garret escreveu um artigo sobre o conjunto de técnicas e tecnologias de assincronismo que já vinham sendo usadas em projetos como Google Maps e Google Suggest, batizando isso tudo como “Ajax”… e… BOOOM!!!

O mundo mudou a partir daí.

Callbacks

Geralmente se deseja efetuar alguma ação quando uma operação assíncrona é concluída. Nos primeiro anos do Ajax, a maneira universal de se fazer isso era usando callbacks.

Para quem não conhece, o callback é uma função que é passada como argumento para uma outra função (higher-order function) e, geralmente, só é executado quando alguma operação é concluída ou quando um evento específico ocorre. O cenário de uso mais comum é, certamente, quando requisições Ajax são finalizadas.

Inclusive, callbacks já eram um conceito familiar para quem havia usado funções comosetInterval e setTimeout.

Para os usos mais simples, os callbacks são uma forma eficaz e decente de se controlar fluxos assíncronos. O problema começa com fluxos mais complexos…

Voltemos às pizzas:

Exemplo 3) Eu vou a uma pizzaria, peço uma pizza para viagem no balcão e, enquanto ela é preparada, dou uma passada na livraria ao lado. Assim que meu pedido estiver pronto, quero que a balconista me ligue para eu poder retirá-la. Vou levar a pizza para a casa de um amigo, onde o restante da nossa “tchurma” já está esperando para comer. Depois de lá, iremos correndo para um show, pois não queremos ficar muito longe do palco. Na casa do meu amigo, resolvo não comer da pizza, pois já jantei. Fico mexendo no Youtube enquanto eles lancham. Espero eles terminarem, para finalmente irmos para o show. Chegando lá, preciso ligar para outro amigo que também irá, para combinarmos o local onde nos encontraremos.

Esse fluxo bizonho, representado em forma de callbacks, ficaria mais ou menos assim:

Ahhh o callback hell…

Acredite, esse é um dos mais simples exemplos possíveis do famoso callback hell. As coisas poderiam ficar muito piores do que isso (e geralmente ficavam). O que caracteriza tal anti-pattern é esse belo formato de pirâmide de lado, como você pode notar no exemplo anterior.

Sendo justo com os callbacks, boa parte dos casos de callback hells eram (e continuam sendo) culpa do programador. Normalmente, boas abstrações seriam o suficiente para evitar ou refatorar usos bizarros de callbacks aninhados. Não vou abordar este tópico aqui, mas você pode dar uma lida neste excelente post do Valeri Karpov, o Code Barbarian.

Promises

Desde o começo, as promessas prometiam bastante (desculpa o trocadalho).

A ideia das promises é representar fluxos assíncronos de forma sequencial/vertical/top-down, além de favorecer o tratamento de exceções.

Vejamos como ficaria nosso exemplo pizzalesco em forma de promessas:

One does not simply break a pinkie promise…

Notou a diferença, né? C’mon!

Sem toda aquela indentação piramidal, ficou muito mais fácil de entender cada passo desse fluxo.

Ao final de cada then() é possível retornar:

  • Um valor qualquer, como um objeto, array, string, etc: nesse caso, o próximo then da sequência é executado imediatamente, recebendo o valor passado como parâmetro.
  • Uma outra promessa: foi isso que fizemos no nosso exemplo. Apesar de ainda não termos mostrado como as funções acima criam uma promessa, é óbvio presumirmos que todas essas operações são assíncronas ( orderPizza, waitUntilTheyFinishEating, goToTheShow e makeCallToMyFriend). Para que o próximo then na sequência espere até que uma dessas operações seja concluída, precisamos retornar uma promessa. Uma vez que a promessa for satisfeita, o fluxo segue.

Mas afinal, de forma clara, what the hell is a promise? Bem, estive pensando em como explicar isso de maneira sucinta e cheguei a uma analogia que ou vai te fazer entender isso rapidamente, ou você vai me xingar muito no Twitter e me bloquear no Orkut:

Promises são como cheques pré-datados. Um cheque representa dinheiro e é uma forma do pagador te garantir que você irá receber aquele valor. Se quem te passou o cheque diz que não vai ter o dinheiro até o dia x e você tenta descontá-lo antes disso, não achará nada. No dia certo, porém, você irá conseguir receber seu dinheiro se estiver de posse do cheque. Há também a possibilidade de você se f…. e o pagador te dar um belo de um calote. You never know.

As Promises, por sua vez, representam um valor e são uma maneira da função que a retornou te garantir que você irá receber aquele bendito valor. Quando a função concluir o processamento necessário para te retornar aquilo que ela havia te prometido, você será avisado, receberá o valor esperado e poderá fazer o que quiser com ele. Infelizmente, há muitas funções caloteiras por aí, e algumas vezes você pode acabar não recebendo o que espera por conta de algum imprevisto (exemplo: a conexão caiu no meio de uma requisição).

Olha, não sei se alguém já usou a analogia acima. Se não usou, lembre-se que a ideia foi minha e vê se não vai sair por aí pagando de autor dessa pérola. =)

Vejamos como uma promessa é criada:

OBS: Por razões didáticas, usei o bom e velho método ajax do jQuery. Uma forma mais contemporânea de se fazer uma request é com Fetch API (com polyfill, por favor!). Preferi não usar fetch neste artigo porque ele já retorna uma promise e, como o intuito era mostrar como uma promessa é construída do zero, fica mais fácil de entender assim. Eu sei que o método$.ajax retorna um jQuery.DeferredObject (já faz uns 300 anos que larguei o jQuery, mas ainda não esqueci disso). Como jQuery.DeferredObject <> Native Promise, preferi não confundir o entendimento do leitor, usando a forma “clássica” de invocar esse método, passando callbacks de sucesso e erro. Uma curiosidade para quem ainda se aventura com jQuery: Desde o lançamento do jQuery 3, DeferredObjects são Promise/A+ compatible.

No gist acima, vemos que a função sendMessage instancia e retorna uma nova promessa. O construtor de uma promise recebe como argumento uma função conhecida como executor. O executor, por sua vez, recebe duas outras funções: resolve e reject. Você não precisa se preocupar em fornecer essas duas, elas são injetadas de forma “automágica”, você só precisa chamá-las no momento certo.

resolve: A função resolve deve ser chamada para sinalizar que a promessa foi cumprida, ou “resolvida”. Caso a operação assíncrona que estava sendo executada possua algum retorno (por exemplo: uma lista de usuários), você o passa como argumento para a função resolve. Quando a promessa é resolvida, o primeiro then da cadeia é chamado.

reject: Em caso de alguma falha, como por exemplo a indisponibilidade de um endpoint, a função reject deve ser chamada. Ao executá-la, você estará sinalizando que a promessa falhou e você vai dar aquele “calote gostoso” em quem dependia do seu retorno.

Para tratar exceções em cadeias de then(), você pode usar o método catch():

OBS: Promises possuem formas de uso que vão além do escopo deste artigo. Há, inclusive, libraries excelentes que oferecem Promises on steroids, como o Bluebird.

OBS 2: Só por curiosidade, eis como ficaria o exemplo anterior usando Fetch API.

Async functions

Se dois dos intuitos originais das promises eram clareza de código e manutenibilidade, o JavaScript deu um passo adiante em ambos os aspectos com as async functions.

Diferentemente da transição de callbacks para promessas, que envolve uma nova forma de pensar em fluxos assíncronos, migrar de promessas para async functions é um caminho notavelmente mais straightforward.

As async functions fazem código assíncrono parecer síncrono.

Vamos direto ao código:

Como você pode ver, o código ficou muito mais enxuto, flat e fácil de entender à primeira vista. O que a função orderPizza e as outras três invocadas com await retornam? Promises, baby. Async functions são altamente integráveis com promessas. Tanto, que até retornam promessas:

É importante frisar que só se pode usar await dentro de funções marcadas como async.

Ao encontrar uma declaração await, a instrução seguinte não será executada até que a promessa em andamento seja resolvida. Isso é possível graças à magia dos generators, outro importante recurso do ES2015.

As async functions podem ser encaradas de forma simples como açúcar sintático para promises e generators. Não irei explicar generators nesse artigo, mas caso você deseje entender exatamente o que acontece por trás de uma async function, leia esse maravilhoso capítulo do livro Exploring ES6, do pic* das galáxias do JavaScript, Dr. Axel Rauschmayer.

Resumo

Existem diferentes abordagens para se resolver problemas assíncronos em JavaScript. Tecnicamente, não há um jeito “errado”. Muitas vezes, um bom e velho callback é a solução mais simples e adequada para uma determinada necessidade. Use cada approach da maneira certa, para o problema certo e seja assincronamente feliz, brother!

--

--