Javascript assíncrono: Promises

Esse post é a primeira parte da série Javascript Assíncrono, no próximo posts vamos falar sobre async/await.


Com o avanço das tecnologias frontend e principalmente do Javascript, a programação assíncrona se tornou cada vez mais necessária e comum nas aplicações, principalmente em SPA’s. Desde os primórdios do jQuery possuímos as requisições XMLHttpRequest (AJAX) que faziam um trabalho assíncrono.

Se você programa há um bom tempo sabe que a primeira forma de criar um código assíncrono no Javascript era utilizando os callbacks da seguinte forma:


function asyncFunction(params, callback, err) {
  var response = // requisição assíncrona
  if (response.status === true) {
    callback(response.data);
  } else {
    err(response.data);
  }
}

asyncFunction(
  params, 
  function(response) {
    console.log('Sucesso!');
  },
  function(err) {
    console.log('Erro!');
  }
);

A boa notícia é que os tempos mudaram e com os avanços da ECMAScript (padrões estipulados para utilização do Javascript) podemos escrever código assíncrono de forma muito menos verbosa e mais funcional.

Promises

Presente de forma nativa no Javascript desde 2015 no ES6, as Promises são classes que são utilizadas pela maioria das bibliotecas de requisições HTTP atualmente como Axios, jQuery, API Fetch, entre outras. A Promise é facilmente identificada por códigos que utilizam o .then para callback de sucesso e .catch para erros. Vamos ver como criar uma Promise:

const divDelayed = (a, b) => new Promise((resolve, reject) => {
  setTimeout(() => {
    if (b == 0) reject('O valor de B não pode ser zero!');

    resolve(a / b);
  }, 2000);
});

No exemplo acima estou criando uma função que recebe dois parâmetros, o retorno dessa função é uma nova Promise que sempre recebe como parâmetro uma função com dois parâmetros, resolve e reject.

O resolve é chamado quando temos uma resposta de sucesso ativando o .then, e o reject é chamado quando temos um erro disparando o .catch.

Na função adicionei um setTimeout para torná-la assíncrona e devolver o valor da divisão ou erro após dois segundos. No caso, se o valor do divisor for zero, um erro é disparado (reject), em qualquer outro caso a divisão é realizada e retornada pelo resolve.

Utilizando a função acima em nosso código com divisor zero teríamos:

divDelayed(10, 0)
  .then(resp => console.log(`Sucesso: ${resp}`))
  .catch(err => console.log(err));

// O valor de B não pode ser zero!

Veja que apenas o .catch foi disparado pois nossa Promise caiu no reject e todo esse processo só irá acontecer após os dois segundos estipulados no setTimeout.

Agora se utilizarmos com um divisor maior que zero:

divDelayed(10, 5)
  .then(resp => console.log(`Sucesso: ${resp}`))
  .catch(err => console.log(err));

// Sucesso: 2

O .then é devidamente disparado com a resposta da divisão.

Encadeamento

Em alguns casos teremos uma Promise que retorna outra Promise e para acessar o resultado dessa segunda iremos precisar encadear as chamadas:

promise1()
  .then(resp => resp.promise2())
  .then(resp2 => console.log(resp2));

Nesse caso, retornamos a chamada da segunda Promise dentro do .thenda primeira, e encadeamos outro .then logo abaixo para tratar o resultado da segunda.

Cascata

Por estarmos tratando de código assíncrono você provavelmente irá utilizar Promises em casos que você precisa esperar pelo resultado para realizar alguma ação e novamente, essa ação que está aguardando pode ser uma Promise, confuso? Vamos ao código. Imaginemos que estamos buscando os dados de um usuário pelo seu username e a partir disso queremos buscar todos seus endereços cadastrados, veja, duas chamadas, porém a segunda depende do sucesso da primeira (nesse caso o api.getserá nossa Promise de busca na API):

api.get('/users/diego3g').then(user => {
  api.get(`/addresses/${user.id}`).then(addresses => {
    console.log(addresses);
  });
});

Essa organização é bonita até certo ponto, a partir do momento que precisamos começar a stackar muitas Promises o código vai se tornando muito verboso:

api.get('/users/diego3g').then(user => {
  api.get(`/groups/${user.id}`).then(groups => {
    groups.map(group => {
      api.get(`/group/${group.id}`).then(groupInfo => {
        console.log(groupInfo);
      });
    })
  });
});

Nesse emaranhado de .then dificilmente conseguimos bater o olho e enxergar o que é o que e por isso no post sobre async/await dessa série vou te explicar como organizar melhor tudo isso.

Promises simultâneas

Em alguns casos você necessita aguardar o resultado de múltiplas Promises porém nenhuma depende da outra para executar, nesse caso você pode utilizar o método Promise.all para aguardar por todas elas:

Promise.all([
  api.get('/users/diego3g'),
  api.get('/phones/diego3g'),
]).then([user, phones] => {
  console.log(user);
  console.log(phones);
});

Passamos um vetor de Promises ao método e apenas um .thenrecuperando o valor de cada Promise separadamente utilizando desestruturação de array do ES6, nesse caso temos o valor dos dados do usuário e telefones separados.

Corrida

Uma das funcionalidades desconhecidas porém muito útil da Promise é o método race que nos permite criar uma corrida entre múltiplas Promises recuperando o resultado da que for mais rápido. Essa corrida é muito útil para adicionar timeout à requisições assíncronas:

const timeout = ms => new Promise((resolve, reject) => 
  setTimeout(reject, ms, { timeout: true });
);

Promise.race([api.get('/users/diego3g'), timeout(2000)])
  .then(resp => console.log(resp))
  .catch(err => console.log(err));

No caso acima, criamos uma função timeout que apenas retorna um reject após tanto tempo e iniciamos uma corrida entre uma Promise de busca de dados do usuário e a Promise de timeout com dois segundos. Ou seja, se a Promise demorar mais que dois segundos, o retorno no .catchserá: { timeout: true }, agora se demorar menos que isso, os dados do usuário devem ser retornados no .then.

Concluindo

Nesse post vimos como utilizar as Promises a nosso favor e organizar as requisições múltiplas quando dependemos ou não do resultado das Promises anteriores utilizando o modelo de cascata e o Promise.all.

No próximo post dessa série vamos ver sobre async/await.

Se você gostou do post, não esquece de deixar suas ?