Artigo original: JavaScript Promise Tutorial – How to Resolve or Reject Promises in JS

Promises são blocos de construção importantes para operações assíncronas em JavaScript. Você pode pensar que as promises não são tão fáceis de entender, aprender e usar – acredite, você não está sozinho!

As promises (em português, promessas) são um desafio para muitos desenvolvedores da Web, mesmo depois de passar anos trabalhando com elas.

Neste artigo, quero tentar mudar essa percepção ao compartilhar o que aprendi sobre promises em JavaScript nos últimos anos. Espero que seja útil para você.

O que é uma promise no JavaScript?

Uma Promise é um objeto especial no JavaScript. Ela produz um valor após uma operação assícrona (também conhecida como async) ser concluída com êxito, ou um erro se ela não for concluída com sucesso, seja porque excedeu o limite de tempo, por erro de rede etc.

As conclusões de chamadas bem-sucedidas são indicadas pela chamada da função resolve e os erros são indicados pela chamada da função reject.

Você pode criar uma promise usando o construtor de promises da seguinte forma:

let promise = new Promise(function(resolve, reject) {    
    // Faz uma chamada assíncrona e conclui ou rejeita essa chamada
});

Na maioria dos casos, uma promise pode ser usada para uma operação assíncrona. Entretanto, tecnicamente, você pode concluir/rejeitar operações síncronas e assíncronas.

Espere aí, não temos funções de callback para operações assíncronas?

Sim, está correto. Temos funções de callback no JavaScript. Porém, uma função de callback não é algo especial no JavaScript. É uma função normal que produz resultados após a conclusão de uma chamada assíncrona (com sucesso/erro).

A palavra "assíncrona" significa que algo acontece no futuro, não agora. Normalmente, callbacks são usadas apenas para fazer coisas como chamadas de rede ou fazer upload/download de coisas, se comunicar com bancos de dados e assim por diante.

Embora callbacks sejam úteis, elas também têm uma grande desvantagem. Às vezes, podemos ter uma callback dentro de outra callback que está em outra callback e assim por diante. Sério! Vamos entender esse "inferno de callbacks" (do inglês, callback hell) com um exemplo.

Como evitar o inferno de callbacks – exemplo do PizzaHub

Vamos pedir uma pizza Marguerita vegana 🍕 do PizzaHub. Quando fazemos o pedido, o PizzaHub detecta automaticamente nossa localização, encontra uma pizzaria próxima e verifica se a pizza que estamos pedindo está disponível.

Se estiver disponível, ele detecta que tipo de bebida recebemos gratuitamente junto com a pizza e, por fim, faz o pedido.

Se o pedido for feito com sucesso, receberemos uma mensagem com uma confirmação.

Então, como podemos codificar isso usando funções de callback? Eu criei algo parecido com isso:

function pedirPizza(tipo, nome) {
    
    // Consulta a pizzahub por uma loja
    query(`/api/pizzahub/`, function(resultado, erro){
       if (!error) {
           let idLoja = resultado.idLoja;
           
           // Obtém a loja e consulta as pizzas
           query(`/api/pizzahub/pizza/${idLoja}`, function(resultado, erro){
               if (!erro) {
                   let pizzas = resultado.pizzas;
                   
                   // Verifica se minha pizza está disponível
                   let minhaPizza = pizzas.find((pizza) => {
                       return (pizza.tipo===tipo && pizza.nome===nome);
                   });
                   
                   // Verifique se há bebidas gratuitas
                   query(`/api/pizzahub/bebidas/${minhaPizza.id}`, function(resultado, erro){
                       if (!erro) {
                           let bebida = resultado.id;
                           
                           // Prepara um pedido
                           query(`/api/pedido`, {'tipo': tipo, 'nome': nome, 'bebida': bebida}, function(resultado, erro){
                              if (!erro) {
                                  console.log(`Seu pedido de ${tipo} ${nome} com ${bebida} foi realizado`);
                              } else {
                                  console.log(`Infelizmente, não temos pizzas para você hoje!`);
                              }
                           });

                       }
                   })
               }
           });
       } 
    });
}

// Chama a função orderPizza
pedirPizza('vegetariana', 'marguerita');

Vamos dar uma olhada na função pedirPizza no código acima.

Ela chama uma API para obter o ID da pizzaria mais próxima. Depois disso, obtém a lista de pizzas disponíveis nesse restaurante. Então, ela verifica se a pizza que estamos pedindo foi encontrada e faz outra chamada à API para encontrar as bebidas para essa pizza. Por fim, a API de pedidos faz o pedido.

Aqui usamos uma callback para cada uma das chamadas de API. Isso nos leva a usar outra callback dentro da anterior, e assim por diante.

Isso significa que entramos em algo que chamamos (de forma muito expressiva) de Inferno de Callbacks. Ninguém quer isso, certo? Isso também forma uma pirâmide de código que não é apenas confusa, mas também propensa a erros.

callback-hell
Demonstração do inferno das callbacks e a pirâmide gerada

Há algumas maneiras de sair do (ou não entrar no) inferno de callbacks. A mais comum é usar uma Promise ou função async. No entanto, para entender bem as funções async, primeiro é preciso ter uma boa compreensão das Promises.

Portanto, vamos começar e nos aprofundar nas promises.

Entendendo os estados das promises

Apenas para revisar, uma promise pode ser criada com a sintaxe de construtor, dessa maneira:

let promise = new Promise(function(resolve, reject) {
  // Código a ser executado
});

A função construtora recebe uma função como argumento. Essa função é chamada de função executora.

// Função executora passada como um argumento 
// para o construtor da Promise
function(resolve, reject) {
    // Sua lógica entra aqui...
}

A função executora recebe dois argumentos: resolve e reject. Essas são as funções de callback fornecidas pela linguagem JavaScript. Sua lógica está dentro da função executora, que é executada automaticamente quando uma new Promise é criada.

Para que a promise seja eficaz, a função executora deve chamar uma das funções de callback, resolve ou reject. Aprenderemos mais sobre isso em detalhes daqui a pouco.

O construtor new Promise() retorna um objeto promise. Como a função executora precisa lidar com operações assíncronas, o objeto promise retornado deve ser capaz de informar quando a execução foi iniciada, concluída (resolved) ou retornada com erro (rejected).

Um objeto promise tem as seguintes propriedades internas:

  1. state – essa propriedade pode ter os seguintes valores:
  • pending – inicialmente, quando a função executora inicia a execução.
  • fulfilled – quando a promise é concluída.
  • rejected – quando a promise é rejeitada.
states_1
Estados da promise

2.  result – essa propriedade pode ter os seguintes valores:

  • undefined – inicialmente, quando o valor do state é pending.
  • value – quando resolve(value) é chamada.
  • error – quando  reject(error) é chamada.

Essas propriedades internas são inacessíveis ao código, mas podem ser inspecionadas. Isso significa que poderemos inspecionar os valores das propriedades state e result usando a ferramenta de depuração, mas não poderemos acessá-las diretamente usando o programa.

promise_state_inspect
É possível inspecionar as propriedades internas de uma promise

O estado de uma promise pode ser pending (pendente), fulfilled (cumprida) ou rejected (rejeitada). Uma promise que é resolvida ou rejeitada é chamada de settled (liquidada).

states_2
Uma promise liquidada é cumprida ou rejeitada

Como as promises são concluídas e rejeitadas

Aqui temos um exemplo de uma promise que será concluída imediatamente (estado fulfilled) com o valor Estou pronto.

let promise = new Promise(function(resolve, reject) {
    resolve("Estou pronto");
});

A promise abaixo será rejeitada (estado rejected) com a mensagem de erro Algo não está correto!.

let promise = new Promise(function(resolve, reject) {
    reject(new Error('Algo não está correto!'));
});

Um ponto importante a observar:

Uma função executora de promises deve chamar apenas uma função resolve ou uma função reject. Uma vez que um estado seja alterado (pending => fulfilled ou pending => rejected), acabou. Qualquer outra chamada para resolve ou para reject será ignorada.

let promise = new Promise(function(resolve, reject) {
  resolve("Com certeza serei resolvido!");

  reject(new Error('Isso será ignorado?')); // ignorado
  resolve("Ignorado?"); // ignorado
});

No exemplo acima, apenas o primeiro a ser resolvido será chamado e os demais serão ignorados.

Como lidar com uma promise depois de criá-la

Uma Promise usa uma função executora para concluir uma tarefa (principalmente de maneira assíncrona). Uma função consumidora (que usa um resultado da promise) deve ser notificada quando a função executora concluir (com sucesso) ou rejeitar (com erro).

Os métodos, .then(), .catch() e .finally(), ajudam a criar o vínculo entre a função executora e as funções consumidoras para que possam estar em sincronia quando uma promise for concluída (resolve) ou rejeitada (reject).

consumer_executor
Funções executora e consumidora

Como usar o método .then() da promise

O método .then() deve ser chamado no objeto promise para lidar com um resultado (resolve) ou um erro (reject).

Aceita duas funções como parâmetros. Normalmente, o método .then() deve ser chamado a partir da função consumidora em que você gostaria de saber o resultado da execução de uma promise.

promise.then(
  (result) => { 
     console.log(result);
  },
  (error) => { 
     console.log(error);
  }
);

Se estiver interessado apenas em resultados bem-sucedidos, pode passar apenas um argumento, desta maneira:

promise.then(
  (result) => { 
      console.log(result);
  }
);

Se você estiver interessado apenas no erro, poderá passar null para o primeiro argumento, assim:

promise.then(
  null,
  (error) => { 
      console.log(error)
  }
);

No entanto, pode tratar melhor os erros usando o método .catch(), que veremos em um minuto.

Vamos dar uma olhada em alguns exemplos de tratamento de resultados e erros usando os métodos .then e .catch . Tornaremos esse aprendizado um pouco mais divertido com algumas requisições assíncronas reais. Usaremos a PokeAPI para obter informações sobre Pokémons e concluí-las/rejeitá-las usando promises.

Primeiro, vamos criar uma função genérica que aceita um URL da PokeAPI como argumento e retorna uma promise. Se a chamada à API for bem-sucedida, uma promise concluída será retornada. Uma promise rejeitada é retornada para qualquer tipo de erro.

A partir de agora, usaremos essa função em vários exemplos para obter uma promise e trabalhar com ela.

function getPromise(URL) {
  let promise = new Promise(function (resolve, reject) {
    let req = new XMLHttpRequest();
    req.open("GET", URL);
    req.onload = function () {
      if (req.status == 200) {
        resolve(req.response);
      } else {
        reject("Ocorreu um erro!");
      }
    };
    req.send();
  });
  return promise;
}
Método utilitário para obter uma promise

Exemplo 1: obter informações de 50 Pokémons:

const ALL_POKEMONS_URL = 'https://pokeapi.co/api/v2/pokemon?limit=50';

// Já discutimos essa função!
let promise = getPromise(ALL_POKEMONS_URL);

const consumer = () => {
    promise.then(
        (result) => {
            console.log({result}); // Exibe o retorno de 50 Pokémons no console do navegador
        },
        (error) => {
            // Como o URL é válido, isso não será chamado.
            console.log('Encontramos um erro!'); // Exibe um erro no console do navegador
    });
}

consumer();

Exemplo 2: vamos tentar um URL inválido

const POKEMONS_BAD_URL = 'https://pokeapi.co/api/v2/pokemon-bad/';

// Isso será rejeitado porque o URL retorna erro 404
let promise = getPromise(POKEMONS_BAD_URL);

const consumer = () => {
    promise.then(
        (result) => {
            // A promise não foi concluída. Portanto, ela não será executada.
            console.log({result});
        },
        (error) => {
            // Uma promise rejeitada executará o seguinte
            console.log('We have encountered an Error!'); // Exibe um erro no console do navegador
        }
    );
}

consumer();

Como usar o método .catch() da promise

Você pode usar esse método para lidar com erros (rejeições) das promises. A sintaxe de passar null como o primeiro argumento para .then() não é uma boa maneira de tratar erros. Portanto, temos o .catch() para fazer o mesmo trabalho com uma sintaxe simples:

// Isso será rejeitado porque o URL retorna 404
let promise = getPromise(POKEMONS_BAD_URL);

const consumer = () => {
    promise.catch(error => console.log(error));
}

consumer();

Se lançarmos um erro usando new Error("Algo está errado!") em vez de chamar o reject da função executora e dos métodos da promise, ele ainda será tratado como uma rejeição. Isso significa que será capturado pelo método .catch.

O mesmo se aplica a todas as exceções síncronas que ocorrem no executor da promise e nas funções.

Aqui está um exemplo onde será tratado como uma rejeição e o método .catch será chamado:

new Promise((resolve, reject) => {
  throw new Error("Algo está errado!");// Sem chamada rejeitada
}).catch((error) => console.log(error)); 

Como usar o método .finally() da promise

O método .finally() realiza tarefas de limpeza como parar um carregamento, fechar uma conexão ativa e assim por diante. O método finally() será chamado independentemente da promise retornar um resultado (resolve) ou um erro (reject). Ele passa o resultado ou o erro para o próximo método, que pode chamar os métodos .then() ou .catch() novamente.

Aqui temos um exemplo que ajudará você a entender os três métodos juntos:

let loading = true;
loading && console.log('Carregando...');

// Obtendo a Promise
promise = getPromise(ALL_POKEMONS_URL);

promise.finally(() => {
    loading = false;
    console.log(`Promessa resolvida e o carregamento é ${loading}`);
}).then((result) => {
    console.log({result});
}).catch((error) => {
    console.log(error)
});

Explicando em detalhes:

  • O método .finally() torna o carregamento false.
  • Se a promise retornar um resultado (resolve), o método .then() será chamado. Se a promise retornar um erro (reject), o método .catch() será chamado. O .finally() será chamado independentemente do resolve ou do reject.

O que é encadeamento de promises?

A chamada  promise.then() sempre retorna uma promise. Essa promise terá o state como pending e result como undefined. Ela nos permite chamar o próximo método .then em uma nova promise.

Quando o primeiro método .then retornar um valor, o próximo método .then pode recebê-lo. O segundo pode passar para o terceiro .then() e assim por diante. Isto forma uma cadeia de métodos .then para transmitir as promises. Esse fenômeno é chamado de Cadeia de Promises.

image-105
Cadeia de promises

Aqui está um exemplo:

let promise = getPromise(ALL_POKEMONS_URL);

promise.then(result => {
    let onePokemon = JSON.parse(result).results[0].url;
    return onePokemon;
}).then(onePokemonURL => {
    console.log(onePokemonURL);
}).catch(error => {
    console.log('In the catch', error);
});

Aqui, primeiro obtemos uma promise resolvida e, em seguida, extraímos o URL para chegar ao primeiro Pokémon. Em seguida, retornamos esse valor e ele será passado como uma promise para a próxima função .then(). Daí o resultado,

https://pokeapi.co/api/v2/pokemon/1/

O método .then pode retornar tanto:

  • Um valor (já vimos isso)
  • Uma nova promise.

Também pode lançar um erro.

Aqui está um exemplo em que criamos uma cadeia de promises com os métodos .then que retornam resultados e uma nova promise:

// Promise Chain with multiple then and catch
let promise = getPromise(ALL_POKEMONS_URL);

promise.then(result => {
    let onePokemon = JSON.parse(result).results[0].url;
    return onePokemon;
}).then(onePokemonURL => {
    console.log(onePokemonURL);
    return getPromise(onePokemonURL);
}).then(pokemon => {
    console.log(JSON.parse(pokemon));
}).catch(error => {
    console.log('In the catch', error);
});

Na primeira chamada de .then, extraímos o URL e o retornamos como um valor. Esse URL será passado para a segunda chamada .then na qual estamos retornando uma nova promise tendo esse URL como argumento.

Essa promise será resolvida e passada para a cadeia, onde obteremos as informações sobre o Pokémon. Aqui está o resultado:

image-159
Resultado da chamada da cadeia de promises

Caso ocorra um erro ou a rejeição de uma promise, o método .catch da cadeia será chamado.

Um ponto a ser observado: chamar .then várias vezes não forma uma cadeia de promises. Você pode acabar fazendo algo assim apenas para introduzir um bug no código:

let promise = getPromise(ALL_POKEMONS_URL);

promise.then(result => {
    let onePokemon = JSON.parse(result).results[0].url;
    return onePokemon;
});
promise.then(onePokemonURL => {
    console.log(onePokemonURL);
    return getPromise(onePokemonURL);
});
promise.then(pokemon => {
    console.log(JSON.parse(pokemon));
});

Chamamos o método .then três vezes na mesma promise, mas não passamos a promise adiante. Isso é diferente da cadeia de promises. No exemplo acima, o resultado será um erro.

image-160

Como lidar com várias promises

Além dos métodos (.then, .catch e .finally), há seis métodos estáticos disponíveis na API da promise. Os primeiros quatro métodos aceitam um array de promises e as executam em paralelo.

  1. Promise.all
  2. Promise.any
  3. Promise.allSettled
  4. Promise.race
  5. Promise.resolve
  6. Promise.reject

Vamos examinar cada uma delas.

O método Promise.all()

Promise.all([promises]) aceita uma coleção (por exemplo, um array) de promises como argumento e as executa em paralelo.

Esse método aguarda a resolução de todas as promises e retorna o array de resultados das promises. Se alguma das promises for rejeitada ou falhar devido a um erro, todos os outros resultados das promises serão ignorados.

Vamos criar três promises para obter informações sobre três Pokémons.

const BULBASAUR_POKEMONS_URL = 'https://pokeapi.co/api/v2/pokemon/bulbasaur';
const RATICATE_POKEMONS_URL = 'https://pokeapi.co/api/v2/pokemon/raticate';
const KAKUNA_POKEMONS_URL = 'https://pokeapi.co/api/v2/pokemon/kakuna';


let promise_1 = getPromise(BULBASAUR_POKEMONS_URL);
let promise_2 = getPromise(RATICATE_POKEMONS_URL);
let promise_3 = getPromise(KAKUNA_POKEMONS_URL);

Use o método Promise.all() passando um array de promises.

Promise.all([promise_1, promise_2, promise_3]).then(result => {
    console.log({result});
}).catch(error => {
    console.log('An Error Occurred');
});

Saída:

image-161

Como você pode ver, o resultado de todas as promises é retornado. O tempo para executar todas as promises é igual ao tempo máximo que a promise leva para ser executada.

O método Promise.any()

Promise.any([promises]) – semelhante ao método all(), .any() também aceita um array de promises para executá-las em paralelo. Esse método não espera que todas as promises sejam resolvidas. Ele finaliza quando qualquer uma das promises é resolvida.

 Promise.any([promise_1, promise_2, promise_3]).then(result => {
     console.log(JSON.parse(result));
 }).catch(error => {
     console.log('An Error Occurred');
 });

A saída seria o resultado de qualquer uma das promises resolvidas:

image-162

O método Promise.allSettled()

promise.allSettled([promises]) – esse método aguarda que todas as promises sejam resolvidas (resolve/reject) e retorna seus resultados como um array de objetos. Os resultados conterão um estado (fulfilled/rejected) e um valor, se for fulfilled. Em caso de status rejected, ele retornará um motivo para o erro.

Aqui está um exemplo de todas as promessas cumpridas (fulfilled):

Promise.allSettled([promise_1, promise_2, promise_3]).then(result => {
    console.log({result});
}).catch(error => {
    console.log('There is an Error!');
});

Saída:

image-163

Se alguma das promises for rejeitada, por exemplo, a promise_1,

let promise_1 = getPromise(POKEMONS_BAD_URL);
image-164

O método Promise.race()

Promise.race([promises]) – Aguarda a primeira promise (a mais rápida) ser concluída e retorna o respectivo resultado/erro.

Promise.race([promise_1, promise_2, promise_3]).then(result => {
    console.log(JSON.parse(result));
}).catch(error => {
    console.log('An Error Occured');
});

Retorna a promise que foi resolvida mais rapidamente:

image-165

O método Promise.resolve/reject

Promise.resolve(value) – Ele resolve uma promise com o valor passado. É o mesmo que o seguinte código:

let promise = new Promise(resolve => resolve(value));

Promise.reject(error) – Rejeita uma promise com o erro passado. É o mesmo que o seguinte código:

let promise = new Promise((resolve, reject) => reject(error));

Podemos reescrever o exemplo da PizzaHub com promises?

Claro, vamos fazer isso. Vamos supor que o método query retornará uma promise. Aqui está um exemplo de método query(). Na vida real, esse método pode se comunicar com um banco de dados e retornar resultados. Nesse caso, ele é bastante codificado, mas tem a mesma finalidade.

function query(endpoint) {
  if (endpoint === `/api/pizzahub/`) {
    return new Promise((resolve, reject) => {
      resolve({'idLoja': '123'});
    })
  } else if (endpoint.indexOf('/api/pizzahub/pizza/') >=0) {
    return new Promise((resolve, reject) => {
      resolve({pizzas: [{'tipo': 'vegetariana', 'nome': 'marguerita', 'id': '123'}]});
    })
  } else if (endpoint.indexOf('/api/pizzahub/bebidas') >=0) {
    return new Promise((resolve, reject) => {
      resolve({id: '10', 'tipo': 'vegetariana', 'nome': 'marguerita', 'bebida': 'coca'});
    })
  } else if (endpoint === `/api/order`) {
    return new Promise((resolve, reject) => {
      resolve({'tipo': 'vegetariana', 'nome': 'marguerita', 'bebida': 'coca'});
    })
  }
}

Em seguida, a refatoração do nosso callback hell. Para isso, primeiro criaremos algumas funções lógicas:

// Retorna um id da loja
let getIdLoja = resultado => resultado.idLoja;

// Retorna uma promise com a lista de pizzas para uma loja
let getListaPizzas = idLoja => {
  const url = `/api/pizzahub/pizza/${idLoja}`;
  return query(url);
}

// Retorna uma promise com a pizza que combina com o pedido do cliente
let getMinhaPizza = (resultado, tipo, nome) => {
  let pizzas = resultado.pizzas;
  let minhaPizza = pizzas.find((pizza) => {
    return (pizza.tipo===tipo && pizza.nome===nome);
  });
  const url = `/api/pizzahub/bebidas/${minhaPizza.id}`;
  return query(url);
}

// Retorna uma promise depois de fazer o pedido
let fazerPedido = resultado => {
  let bebida = resultado.id;
   return query(`/api/pedido`, {'tipo': resultado.tipo, 'nome': resultado.nome, 'bebida': resultado.bebida});
}

// Confirma o pedido
let confirmarPedido = resultado => {
    console.log(`Seu pedido de ${resultado.tipo} ${resultado.nome} com ${resultado.bebida} foi aceito!`);
}

Use essas funções para criar as promises necessárias. É aqui que você deve comparar com o exemplo do callback hell. Este é bem mais legal e elegante.

function pedirPizza(tipo, nome) {
  query(`/api/pizzahub/`)
  .then(resultado => getIdLoja(resultado))
  .then(idLoja => getListaPizzas(idLoja))
  .then(resultado => getMinhaPizza(resultado, tipo, nome))
  .then(resultado => realizarPedido(resultado))
  .then(resultado => confirmarPedido(resultado))
  .catch(function(erro){
    console.log(`Infelimente, não temos pizzas para você hoje!`);
  })
}

Por fim, chame o método pedirPizza() passando o tipo e o nome da pizza, da seguinte forma:

pedirPizza('vegetariana', 'marguerita');

O que vem a seguir?

Se você chegou até aqui e leu a maior parte das linhas acima, parabéns! Agora você já deve ter uma noção melhor das Promises do JavaScript. Todos os exemplos usados neste artigo estão neste repositório do GitHub.

Em seguida, você deve aprender sobre a função async no JavaScript, que simplifica ainda mais as coisas. A melhor maneira de aprender o conceito de promises do JavaScript é escrever pequenos exemplos e desenvolvê-los com base neles.

Independentemente do framework ou da biblioteca (Angular, React, Vue e assim por diante) que usarmos, as operações assíncronas são inevitáveis. Isso significa que precisamos entender as promises para que as coisas funcionem melhor.

Além disso, tenho certeza de que você achará o uso do método fetch muito mais fácil agora:

fetch('/api/user.json')
.then(function(response) {
    return response.json();
})
.then(function(json) {
    console.log(json); // {"name": "tapas", "blog": "freeCodeCamp"}
});
  • O método fetch retorna uma promise. Portanto, podemos chamar o método .then nele.
  • O restante é sobre a cadeia de promises que aprendemos neste artigo.

Antes de terminarmos...

Obrigado por ler até aqui! Vamos nos conectar. Você pode me mandar mensagens no Twitter (@tapasadhikary) com comentários.

Você também pode gostar destes artigos (em inglês):

Isso é tudo por enquanto. Voltaremos a nos ver em breve com meu próximo artigo. Até lá, cuide-se bem. 🙂