Artigo original: How to make a Promise out of a Callback function in JavaScript
Escrito por: Adham El Banhawy
Desenvolvedores de back-end enfrentam desafios o tempo todo ao criar aplicações ou testar código. Por ser um desenvolvedor relativamente novo e que está recém se familiarizando com esses desafios, nunca me deparei com um desafio ou inconveniência com mais frequência — ou mais memorável — do que quando usei funções de callback.
Não vou me aprofundar muito nos detalhes das callbacks e seus prós e contras nem nas alternativas a elas, como promises e async/await. Para uma explicação mais detalhada, você pode conferir este artigo (em inglês) que as explica detalhadamente.
O inferno das callbacks
As callbacks são uma funcionalidade útil do JavaScript que permite realizar chamadas assíncronas. Elas são funções que geralmente são passadas como um segundo parâmetro para outra função que está buscando dados ou realizando uma operação de E/S que leva tempo para completar.
Um exemplo disso é ao tentar fazer uma chamada de API usando o módulo request
ou ao se conectar a um banco de dados do MongoDB. Se ambas as chamadas dependerem uma da outra ou se a informação que você está buscando for o URL do MongoDB ao qual você precisa se conectar, o que você pode fazer?
Nesse caso, é preciso aninhar essas chamadas uma dentro da outra:
request.get(url, function(error, response, mongoUrl) {
if(error) throw new Error("Erro ao buscar dados");
MongoClient.connect(mongoUrl, function(error, client) {
if(error) throw new Error("Erro de conexão com o MongoDB");
console.log("Conectado com sucesso ao servidor");
const db = client.db("dbName");
// Realizar alguma lógica da aplicação
client.close();
});
});
Certo... então, onde está o problema? Bem, por um lado, a legibilidade do código sofre com essa técnica.
Pode parecer aceitável no começo, quando a base de código é pequena, mas a escalabilidade disso não é das melhores, especialmente se você inserir mais camadas profundas nos callbacks aninhados.
Você acabará com muitos colchetes e chaves que confundirão você e outros desenvolvedores, não importando se o código está bem formatado ou não. Existe um site chamado callbackhell (em português, o "inferno das callbacks") que aborda essa questão específica.
Ouço alguns de vocês, incluindo meu eu passado ingênuo, me dizendo para envolver em uma função async
e então await
(em português, esperar) pela função de callback. Isso simplesmente não funciona.
Se houver qualquer bloco de código após a função que usa callbacks, aquele bloco de código será executado e NÃO esperará pela callback.
Aqui está o erro que cometi antes:
var request = require('request');
// ERRADO
async function(){
let joke;
let url = "https://api.chucknorris.io/jokes/random"
await request.get(url, function(error, response, data) {
if(error) throw new Error("Erro ao buscar dados");
let content = JSON.parse(data);
joke = content.value;
});
console.log(joke); // undefined
};
// Errado
async function(){
let joke;
let url = "https://api.chucknorris.io/jokes/random"
request.get(url, await function(error, response, data) {
if(error) throw new Error("Erro ao buscar dados");
let content = JSON.parse(data);
joke = content.value;
});
console.log(joke); // undefined
};
Alguns desenvolvedores mais experientes podem dizer "Basta usar uma biblioteca diferente que use promises para fazer a mesma coisa, como o axios, ou simplesmente usar o fetch". Claro, eu poderia fazer isso nesse cenário, mas é apenas fugir do problema.
Além disso, esse é apenas um exemplo. Às vezes, você pode estar preso a usar uma biblioteca que não suporta promises sem alternativas, como ao usar kits de desenvolvimento de software (SDKs) para se comunicar com plataformas como a Amazon Web Services (AWS), o Twitter ou o Facebook.
Às vezes, até mesmo usar um callback para fazer uma chamada muito simples com uma E/S rápida ou uma operação de CRUD está bem, já que nenhuma outra lógica depende de seus resultados. Você, no entanto, pode estar restrito pelo ambiente de execução, como em uma função Lambda (site em inglês) que mataria todos os processos uma vez que a thread principal terminasse, independentemente de qualquer chamada assíncrona que não tenha sido concluída.
Solução 1 (fácil): usar o módulo "util" do Node
A solução é surpreendentemente simples. Mesmo se você estiver um pouco desconfortável com a ideia de promises no JavaScript, você vai adorar como pode resolver esse problema usando-as.
Como apontado por Erop e Robin, as versões a partir da versão 8 do Node suportam transformar funções de callback em promises usando o módulo interno util.
const request = require('request');
const util = require('util');
const url = "https://api.chucknorris.io/jokes/random";
// Use o util para trasnformar em promise o método request
const getChuckNorrisFact = util.promisify(request);
// Use o novo método para chamar a API em um padrão moderno de then/catch
getChuckNorrisFact(url).then(data => {
let content = JSON.parse(data.body);
console.log('Piada: ', content.value);
}).catch(err => console.log('erro: ', err))
O código acima resolve o problema de maneira limpa usando o método util.promisify, disponível na biblioteca principal do Node.
Solução 2 (um pouco mais complicada): transformar a callback em uma promise
Às vezes, usar as bibliotecas request
e util
não é possível, seja por causa de um ambiente/código legado ou por realizar as requisições a partir do navegador do lado do client. Você precisaria encapsular uma promise em torno de sua função de callback.
Vamos pegar o exemplo do Chuck Norris acima e transformá-lo em uma promise.
var request = require('request');
let url = "https://api.chucknorris.io/jokes/random";
// Uma função que retorna uma promise para resolver os dados obtidos da API ou um erro
let getChuckNorrisFact = (url) => {
return new Promise(
(resolve, reject) => {
request.get(url, function(error, response, data){
if (error) reject(error);
let content = JSON.parse(data);
let fact = content.value;
resolve(fact);
})
}
);
};
getChuckNorrisFact(url).then(
fact => console.log(fact) // realmente exibe uma string
).catch(
error => console.log(error)
);

No código acima, coloquei a função request
baseada em callback dentro de uma promise que a envolve Promise((resolve, reject) => { //função de callback})
. Isso nos permite chamar a função getChuckNorrisFact
como uma promise com os métodos **.then()**
e .catch()
. Quando _getChuckNorrisFact_
é chamada, ela executa a requisição para a API e espera por uma declaração resolve()
ou reject()
para executar. Na função de callback, você simplesmente passa os dados recuperados para os métodos resolve ou reject.
Uma vez que os dados (nesse caso, um fato incrível sobre Chuck Norris) são obtidos e passados para o resolve, getChuckNorrisFact
executa o método then()
. Isso retornará o resultado que você pode utilizar dentro de uma função dentro do then()
para realizar a lógica desejada — que, nesse caso, é exibir o fato no console.
Você pode ler mais sobre isso na documentação da web da MDN.