Promessas de JavaScript: um tutorial com exemplos
Publicados: 2022-03-11Promessas são um tema quente nos círculos de desenvolvimento de JavaScript, e você definitivamente deve se familiarizar com elas. Eles não são fáceis de entender; pode levar alguns tutoriais, exemplos e uma quantidade razoável de prática para compreendê-los.
Meu objetivo com este tutorial é ajudá-lo a entender as promessas de JavaScript e incentivá-lo a praticar mais o uso delas. Estarei explicando o que são promessas, quais problemas elas resolvem e como elas funcionam. Cada etapa, descrita neste artigo, é acompanhada por um exemplo de código jsbin
para ajudá-lo a trabalhar e ser usado como base para exploração adicional.
O que é uma promessa JavaScript?
Uma promessa é um método que eventualmente produz um valor. Pode ser considerado como a contrapartida assíncrona de uma função getter. Sua essência pode ser explicada como:
promise.then(function(value) { // Do something with the 'value' });
As promessas podem substituir o uso assíncrono de retornos de chamada e oferecem vários benefícios sobre elas. Eles começam a ganhar terreno à medida que mais e mais bibliotecas e frameworks os adotam como sua principal maneira de lidar com a assincronicidade. Ember.js é um ótimo exemplo desse framework.
Existem várias bibliotecas que implementam a especificação Promises/A+. Aprenderemos o vocabulário básico e trabalharemos com alguns exemplos de promessas de JavaScript para apresentar os conceitos por trás deles de maneira prática. Usarei uma das bibliotecas de implementação mais populares, rsvp.js, nos exemplos de código.
Prepare-se, vamos rolar muitos dados!
Obtendo a biblioteca rsvp.js
Promises e, portanto, rsvp.js, podem ser usados tanto no servidor quanto no cliente. Para instalá-lo para nodejs , vá para a pasta do seu projeto e digite:
npm install --save rsvp
Se você trabalha no front-end e usa o bower, é apenas um
bower install -S rsvp
um jeito.
Se você quiser apenas entrar no jogo, pode incluí-lo por meio de uma simples tag de script (e com jsbin
, você pode adicioná-lo através do menu suspenso “Adicionar biblioteca”):
<script src="//cdn.jsdelivr.net/rsvp/3.0.6/rsvp.js"></script>
Quais propriedades uma promessa tem?
Uma promessa pode estar em um dos três estados: pendente , cumprida ou rejeitada . Quando criada, a promessa fica em estado pendente. A partir daqui, ele pode ir para o estado preenchido ou rejeitado. Chamamos essa transição de resolução da promessa . O estado resolvido de uma promessa é seu estado final, portanto, uma vez cumprido ou rejeitado, ele permanece lá.
A maneira de criar uma promessa em rsvp.js é por meio do que é chamado de construtor revelador. Esse tipo de construtor recebe um único parâmetro de função e o chama imediatamente com dois argumentos, fulfill
e reject
, que podem fazer a transição da promessa para o estado fulfilled
ou rejected
:
var promise = new RSVP.Promise(function(fulfill, reject) { (...) });
Esse padrão de promessas JavaScript é chamado de construtor revelador porque o argumento de função única revela seus recursos para a função construtora, mas garante que os consumidores da promessa não possam manipular seu estado.
Os consumidores da promessa podem reagir às mudanças de estado adicionando seu manipulador por meio do método then
. É preciso uma função de manipulação de preenchimento e rejeição, ambas as quais podem estar ausentes.
promise.then(onFulfilled, onRejected);
Dependendo do resultado do processo de resolução da promessa, o manipulador onFulfilled
ou onRejected
é chamado de forma assíncrona .
Vamos ver um exemplo que mostra em que ordem as coisas são executadas:
function dieToss() { return Math.floor(Math.random() * 6) + 1; } console.log('1'); var promise = new RSVP.Promise(function(fulfill, reject) { var n = dieToss(); if (n === 6) { fulfill(n); } else { reject(n); } console.log('2'); }); promise.then(function(toss) { console.log('Yay, threw a ' + toss + '.'); }, function(toss) { console.log('Oh, noes, threw a ' + toss + '.'); }); console.log('3');
Este snippet imprime uma saída semelhante à seguinte:
1 2 3 Oh, noes, threw a 4.
Ou, se tivermos sorte, vemos:
1 2 3 Yay, threw a 6.
Este tutorial de promessas demonstra duas coisas.
Primeiro, que os manipuladores que anexamos à promessa foram realmente chamados depois que todos os outros códigos foram executados, de forma assíncrona.
Segundo, que o manipulador de cumprimento foi chamado apenas quando a promessa foi cumprida, com o valor com o qual foi resolvido (no nosso caso, o resultado do lançamento de dados). O mesmo vale para o manipulador de rejeição.
Encadeando promessas e escorrendo
A especificação requer que a função then
(os manipuladores) deva retornar uma promessa também, o que permite encadear promessas, resultando em um código que parece quase síncrono:
signupPayingUser .then(displayHoorayMessage) .then(queueWelcomeEmail) .then(queueHandwrittenPostcard) .then(redirectToThankYouPage)
Aqui, signupPayingUser
retorna uma promessa e cada função na cadeia de promessas é chamada com o valor de retorno do manipulador anterior depois de concluída. Para todos os propósitos práticos, isso serializa as chamadas sem bloquear o thread de execução principal.
Para ver como cada promessa é resolvida com o valor de retorno do item anterior na cadeia, voltamos ao lançamento de dados. Queremos lançar os dados no máximo três vezes, ou até que os seis primeiros apareçam jsbin:
function dieToss() { return Math.floor(Math.random() * 6) + 1; } function tossASix() { return new RSVP.Promise(function(fulfill, reject) { var n = Math.floor(Math.random() * 6) + 1; if (n === 6) { fulfill(n); } else { reject(n); } }); } function logAndTossAgain(toss) { console.log("Tossed a " + toss + ", need to try again."); return tossASix(); } function logSuccess(toss) { console.log("Yay, managed to toss a " + toss + "."); } function logFailure(toss) { console.log("Tossed a " + toss + ". Too bad, couldn't roll a six"); } tossASix() .then(null, logAndTossAgain) //Roll first time .then(null, logAndTossAgain) //Roll second time .then(logSuccess, logFailure); //Roll third and last time
Ao executar este código de exemplo de promessas, você verá algo assim no console:
Tossed a 2, need to try again. Tossed a 1, need to try again. Tossed a 4. Too bad, couldn't roll a six.
A promessa retornada por tossASix
é rejeitada quando o lance não é um seis, então o manipulador de rejeição é chamado com o lance real. logAndTossAgain
imprime esse resultado no console e retorna uma promessa que representa outro lançamento de dados. Esse lance, por sua vez, também é rejeitado e desconectado pelo próximo logAndTossAgain
.
Às vezes, no entanto, você tem sorte* e consegue rolar um seis:

Tossed a 4, need to try again. Yay, managed to toss a 6.
* Você não precisa ter essa sorte. Há uma chance de ~42% de rolar pelo menos um seis se você rolar três dados.
Esse exemplo também nos ensina algo mais. Vê como não foram feitos mais lançamentos após o primeiro lançamento bem sucedido de um seis? Observe que todos os manipuladores de cumprimento (os primeiros argumentos nas chamadas para then
) na cadeia são null
, exceto o último, logSuccess
. A especificação exige que, se um manipulador (cumprimento ou rejeição) não for uma função, a promessa retornada deve ser resolvida (cumprida ou rejeitada) com o mesmo valor. No exemplo de promessas acima, o manipulador de cumprimento, null
, não é uma função e o valor da promessa foi cumprido com um 6. Portanto, a promessa retornada pela chamada then
(a próxima na cadeia) também será cumprida com 6 como seu valor.
Isso se repete até que um manipulador de atendimento real (um que seja uma função) esteja presente, de modo que o atendimento desça até que seja tratado. No nosso caso, isso acontece no final da cadeia, onde é alegremente desconectado do console.
Manipulação de erros
A especificação Promises/A+ exige que, se uma promessa for rejeitada ou um erro for lançado em um manipulador de rejeição, ele deve ser tratado por um manipulador de rejeição que esteja “a jusante” da fonte.
Aproveitar a técnica de trickle down abaixo oferece uma maneira limpa de lidar com erros:
signupPayingUser .then(displayHoorayMessage) .then(queueWelcomeEmail) .then(queueHandwrittenPostcard) .then(redirectToThankYouPage) .then(null, displayAndSendErrorReport)
Como um manipulador de rejeição é adicionado apenas no final da cadeia, se algum manipulador de atendimento na cadeia for rejeitado ou lançar um erro, ele escorre até encontrar displayAndSendErrorReport
.
Vamos voltar aos nossos dados amados e ver isso em ação. Suponha que queremos apenas jogar dados de forma assíncrona e imprimir os resultados:
var tossTable = { 1: 'one', 2: 'two', 3: 'three', 4: 'four', 5: 'five', 6: 'six' }; function toss() { return new RSVP.Promise(function(fulfill, reject) { var n = Math.floor(Math.random() * 6) + 1; fulfill(n); }); } function logAndTossAgain(toss) { var tossWord = tossTable[toss]; console.log("Tossed a " + tossWord.toUppercase() + "."); } toss() .then(logAndTossAgain) .then(logAndTossAgain) .then(logAndTossAgain);
Quando você executa isso, nada acontece. Nada é impresso no console e nenhum erro é lançado, aparentemente.
Na realidade, um erro é lançado, simplesmente não o vemos, pois não há manipuladores de rejeição na cadeia. Como o código nos manipuladores é executado de forma assíncrona, com uma pilha nova, ele nem é desconectado do console. Vamos corrigir isso:
function logAndTossAgain(toss) { var tossWord = tossTable[toss]; console.log("Tossed a " + tossWord.toUpperCase() + "."); } function logErrorMessage(error) { console.log("Oops: " + error.message); } toss() .then(logAndTossAgain) .then(logAndTossAgain) .then(logAndTossAgain) .then(null, logErrorMessage);
A execução do código acima mostra o erro agora:
"Tossed a TWO." "Oops: Cannot read property 'toUpperCase' of undefined"
Esquecemos de retornar algo de logAndTossAgain
e a segunda promessa é cumprida com undefined
. O próximo manipulador de atendimento explode tentando chamar toUpperCase
sobre isso. Essa é outra coisa importante a ser lembrada: sempre devolva algo dos manipuladores ou esteja preparado em manipuladores subsequentes para que nada seja passado.
Construindo mais alto
Já vimos o básico das promessas de JavaScript no código de exemplo deste tutorial. Um grande benefício de usá-los é que eles podem ser compostos de maneiras simples para produzir promessas “compostas” com o comportamento que gostaríamos. A biblioteca rsvp.js
fornece um punhado deles, e você sempre pode criar seus próprios usando os primitivos e esses de nível superior.
Para o exemplo final e mais complexo, viajamos para o mundo do RPG de AD&D e jogamos dados para obter pontuações de personagens. Tais pontuações são obtidas rolando três dados para cada perícia do personagem.
Deixe-me colar o código aqui primeiro e depois explicar o que há de novo:
function toss() { var n = Math.floor(Math.random() * 6) + 1; return new RSVP.resolve(n); // [1] } function threeDice() { var tosses = []; function add(x, y) { return x + y; } for (var i=0; i<3; i++) { tosses.push(toss()); } return RSVP.all(tosses).then(function(results) { // [2] return results.reduce(add); // [3] }); } function logResults(result) { console.log("Rolled " + result + " with three dice."); } function logErrorMessage(error) { console.log("Oops: " + error.message); } threeDice() .then(logResults) .then(null, logErrorMessage);
Estamos familiarizados com o toss
do último exemplo de código. Ele simplesmente cria uma promessa que sempre é cumprida com o resultado do lançamento de um dado. Eu usei RSVP.resolve
, um método conveniente que cria tal promessa com menos cerimônia (veja [1] no código acima).
Em threeDice
, criei 3 promessas que cada uma representa um lançamento de dados e finalmente as combinei com RSVP.all
. RSVP.all
recebe uma matriz de promessas e é resolvido com uma matriz de seus valores resolvidos, um para cada promessa constituinte, mantendo sua ordem. Isso significa que temos o resultado dos lançamentos em results
(veja [2] no código acima), e retornamos uma promessa que é cumprida com sua soma (veja [3] no código acima).
Resolver a promessa resultante registra o número total:
"Rolled 11 with three dice"
Usando promessas para resolver problemas reais
As promessas de JavaScript são usadas para resolver problemas em aplicativos que são muito mais complexos do que lançamentos de dados assíncronos por nenhum motivo .
Se você substituir o lançamento de três dados pelo envio de três solicitações ajax para endpoints separados e continuar quando todos eles retornarem com sucesso (ou se algum deles falhar), você já terá uma aplicação útil de promessas e RSVP.all
.
As promessas, quando usadas corretamente, produzem código fácil de ler que é mais fácil de raciocinar e, portanto, mais fácil de depurar do que retornos de chamada. Não há necessidade de estabelecer convenções sobre, por exemplo, tratamento de erros, pois já fazem parte da especificação.
Nós mal arranhamos a superfície do que as promessas podem fazer neste tutorial de JavaScript. As bibliotecas Promise fornecem uma boa dúzia de métodos e construtores de baixo nível que estão à sua disposição. Domine-os, e o céu é o limite do que você pode fazer com eles.
Sobre o autor
Balint Erdi foi um grande fã de RPG e AD&D há muito tempo, e é uma grande promessa e fã de Ember.js agora. O que tem sido constante é sua paixão pelo rock & roll. Por isso decidiu escrever um livro sobre o Ember.js que usa o rock & roll como tema da aplicação do livro. Inscreva-se aqui para saber quando for lançado.