Trabalhando com Promises no JavaScript

14 de janeiro de 2017

Uma das grandes novidades que vieram no ES6 foram as Promises. Mas afinal, o que são? De onde vem? Do que se alimentam?

Promessa é dívida

Promise vem do inglês, que significa promessa. E é mais ou menos isso: uma tarefa que é executada de forma assíncrona com a promessa de devolver alguma resposta, assim que concluída. Não é novidade fazer este tipo de requisições no JavaScript. Com a ausência de uma API específica, utilizava-se alguns workarounds para que este recurso funcionasse corretamente.

function getData(url, callback) {
  var xhr = new XMLHttpRequest();

  xhr.onload = function() {
    if (xhr.readyState === XMLHttpRequest.DONE) {
      if (xhr.status === 200) {
        callback(xhr.responseText);
      }
    }
  };

  xhr.open('GET', url, true);
  xhr.send();
}

Por exemplo, dada esta função acima, eu consigo fazer uma chamada em um web service e, assim que obtiver a resposta, executo alguma ação com ela, como, por exemplo, exibir no console. Para isso, basta eu passar uma função como parâmetro, que é o que chamamos de callback.

getData('https://pointer.alancesar.org/v1/place/-23.525889,-46.678908/pt', function(data) {
  console.log(data)
});

Até aí tudo bem. Mas imagine que esta minha requisição falhe. Minha aplicação nunca irá saber. Podemos adaptar o código para lançar um erro, caso as coisas não aconteçam como o esperado.

function getData(url, callback, error) {
  var xhr = new XMLHttpRequest();

  xhr.onload = function() {
    if (xhr.readyState === XMLHttpRequest.DONE) {
      if (xhr.status === 200) {
        callback(xhr.responseText);
      } else {
        error('Erro na requisição!');
      }
    }
  };

  xhr.open('GET', url, true);
  xhr.send();
}

Agora, além de uma função callback, eu passo como parâmetro outra função para ser executada em caso de falha.

getData('https://pointer.alancesar.org/v1/place/-23.525889,-46.678908/pt', function(data) {
  console.log(data)
}, function(msg) {
  console.error(msg)
});

Ficou mais complexo? Mas ainda assim funciona, né? Agora, se invés de um GET, eu tiver um método POST?

function postData(url, data, callback, error) {
  var xhr = new XMLHttpRequest();

  xhr.onload = function() {
    if (xhr.readyState === XMLHttpRequest.DONE) {
      if (xhr.status === 200) {
        callback(xhr.responseText);
      } else {
        error('Erro na requisição!');
      }
    }
  };

  xhr.open('POST', url, true);
  xhr.send(JSON.stringify(data));
}

E o nosso cliente, que fará o uso deste método, deseja apenas fazer o POST e nada mais, pouco se importando se dará certo ou não?

postData('https://www.google.com.br', { 'busca': 'JavaScript' });

Olha o que vai acontecer:

Uncaught TypeError: error is not a function at XMLHttpRequest.xhr.onload (<anonymous>:9:9)

O interpretador não encontrou o parâmetro error. Mas não mesmo, nosso cliente não o informou. Consegue imaginar alguma solução?

O inferno dos callbacks

Para resolver este pequeno problema, sem ter que implementar métodos que nunca retornarão nada, pode ser acrescentada algumas verificação, tanto no callback quanto no error.

function postData(url, data, callback, error) {
  var xhr = new XMLHttpRequest();

  xhr.onload = function() {
    if (xhr.readyState === XMLHttpRequest.DONE) {
      if (xhr.status === 200) {
        if (callback) {
          callback(xhr.responseText);
        }
      } else {
        if (error) {
          error('Erro na requisição!');
        }
      }
    }
  };

  xhr.open('POST', url, true);
  xhr.send(JSON.stringify(data));
}

Dessa forma, caso o parâmetro não seja informado, o método apenas o ignora e vida que segue. Mas começou a ficar feio. Agora, pra piorar, imagine a necessidade de executar um postData() com os dados obtidos no getData()?

getData('https://pointer.alancesar.org/v1/place/-23.525889,-46.678908/pt', function(data) {
  postData('https://www.google.com.br', { 'busca': data.result }, function(data) {
    console.log(data)
  }, function(msg) {
    console.error(msg)
  });
}, function(msg) {
  console.error(error)
});

Chamamos essa aberração de inferno de callbacks e você deve imaginar porquê. Para resolver este empecilho, foi introduzido no ECMAScript 2015 as Promises. Elas fazem basicamente o mesmo serviço sujo, só que de uma forma bem mais elegante.

function getData(url) {
  return new Promise(function(resolve, reject) {
    var xhr = new XMLHttpRequest();

    xhr.onload = function() {
      if (xhr.readyState === XMLHttpRequest.DONE) {
        if (xhr.status === 200) {
          resolve(xhr.responseText);
        } else {
          reject('Erro na requisição!');
        }
      }
    };

    xhr.open('GET', url, true);
    xhr.send();
  });
}

A princípio, parece apenas uma mudança na sintaxe que só deixa o código mais complexo ainda. Mas ao executar as chamadas, começamos a perceber suas vantagens.

getData('https://pointer.alancesar.org/v1/place/-23.525889,-46.678908/pt').then(function(data) {
  console.log(data);
});

O método then() é executado no sucesso da requisição. É o que foi informado no resolve(), quando implementamos o método. Para tratarmos exceções, temos o catch(), que é o que foi passado no método reject().

getData('https://pointer.alancesar.org/v1/place/-23.525889,-46.678908/pt').then(function(data) {
  console.log(data);
}).catch(function(msg) {
  console.error(msg);
});

Note que, por retornar um objeto Promise, eu posso armazenar tudo isso em uma variável.

var promise = getData('https://pointer.alancesar.org/v1/place/-23.525889,-46.678908/pt');

// Algum código que não depende da resposta da requisição
// ...

promise.then(function(data) {
  console.log(data);
});

E, de brinde, podemos deixar o código mais enxuto fazendo uso das Arrow Functions.

function getData(url) {
  return new Promise((resolve, reject) => {
    var xhr = new XMLHttpRequest();

    xhr.onload = function() {
      if (xhr.readyState === XMLHttpRequest.DONE) {
        if (xhr.status === 200) {
          resolve(xhr.responseText);
        } else {
          reject('Erro na requisição!');
        }
      }
    };

    xhr.open('GET', url, true);
    xhr.send();
  });
}

getData('https://pointer.alancesar.org/v1/place/-23.525889,-46.678908/pt')
  .then(data => console.log(data))
  .catch(msg => console.error(msg));
});

Escrito por Alan Cesar, Software Engineer na Pismo.
Trabalhando com desenvolvimento desde 2014, mas fazendo código desde moleque.

© alancesar.dev, 2021 | Built with Gatsby.