graciano codes

Corrente e cadeado numa porta de madeira, com o efeito 3D de sempre

Promessas em javascript

# Antes de explicar o que é Promessa, uma Promessa

Eu prometo que vou explicar as promises, mas antes, vou trazer aqui o contexto de como elas surgiram, porque o mundo real não é um tutorial isolado em que tudo funciona… e você pode acabar se deparando com um sistema legado cheio de callbacks por aí.

# Callbacks

Em Javascript, funções são um tipo de variável, por exemplo const minhaFunc = () => 'olá mundo'. Que, caso você não conheça ainda as arrow functions, é a mesma coisa que:

const minhaFunc = function() {
    return 'olá mundo';
}

Uma outra maneira de escrever a mesma coisa (porém redundante, porque a função não faz nada além de retornar a string), seria:

const minhaFunc = () => {
    return 'olá mundo';
}

Sendo as funções um tipo, pode-se passar uma função como argumento de outra, para ela ser executada no futuro. A esta função que é argumento de outra, dá-se o nome de callback. Um exemplo disso existe desde a idade da pedra da linguagem e se chama setTimeout, por exemplo setTimeout(() => console.log('Julinho da Van'), 3000) printa “Julinho da Van” no seu console depois de 3 segundos (ou 3000 milissegundos).

Então, quem ainda usa jQuery deve conhecer a função $.ajax.

$.ajax({
    url: 'https://swapi.co/api/people/5/',
    success: function(princesaLeia) {
        console.log('dados da Princesa Leia', princesaLeia);
    },
    error: function(request, status, error) {
        console.log('erro de acesso na api do Star Wars', request.responseText);
    }
}).done(function() {
      console.log('terminei');
  });

Esse tipo de abordagem acabou se padronizando para o uso de um callback único, que informa um erro no primeiro argumento e os argumentos de sucesso em sequência como callback(erro, dados), por exemplo:

const retornaAlgoDoBanco = (parametros, booleanAleatorio, callback) => {
    // chama o banco usando os parametros e o booleanAleatorio
    // caso dê erro, "retorna" o erro, desta maneira
    callback(erro);
    // caso de sucesso, o erro é nulo e os dados são "retornados" no segundo argumento
    callback(null, dados);
}

Agora imagine se precisamos fazer chamadas dessas em sequência:

retornaAlgoDoBanco({
    id: 1
}, true, (err, dados) => {
    if (err) {
        console.log(err);
        // interrompe a execução da função, pois não há dados para serem usados
        return;
    }
    outraFuncComCalback(dados, (err, retornoDessaOutraFunc) => {
        if (err) {
            console.log(err);
            return;
        }
        usa(dados);
        retornaAlgoDoBanco({
            id: 2
        }, true, (err, maisDadosAinda) => {
            if (err) {
                console.log(err);
                return;
            }
            usa(maisDadosAinda);
        })
    });
});

Ao exemplo de cima damos o nome de callback hell. E alguém genial traduziu para “pirâmide da desgraça” no guia da Mozilla em português.

# Promises

Quando alguém promete alguma coisa, como foi o meu caso ali em cima, só existem duas possibilidades: ou cumpre-se a promessa, ou se rejeita a ação. No Javascript é a mesma coisa. Ironicamente, a declaração de uma Promise se dá com um callback.

const promessaDoBanco = (parametros, booleanAleatorio) => new Promise((resolve, reject) => {
    // chama o banco usando os parametros e o booleanAleatorio
    // caso dê erro, "retorna" o erro, desta maneira
    reject(erro);
    // caso de sucesso "retorna" os dados
    resolve(dados);
});

Porém, raríssimas são as vezes em que o programador declara uma nova Promise. Em geral, você vai usar uma API que retorna uma Promise, e cabe ao programador lidar com o sucesso ou a rejeição da Promise. Por exemplo, a função fetch faz exatamente o que o antigo ajax do jQuery fazia:

fetch('https://swapi.co/api/people/5/')
    .then(res => console.log('dados da Princesa Leia', res.json()))
    .catch(err => console.log('erro de acesso na api do Star Wars', err));

Uma maneira de consertar a pirâmide da desgraça ali de cima seria com uma função promisify, por exemplo promisify(retornaAlgoDoBanco), que retorna uma função que retorna uma Promise. Em node (mas também é possível usar no browser), o uso seria algo como:

const { promisify } = require('util'); // o pacote 'util' já vem no node nativo, a partir da versão 8
const promessaDeAlgoDoBanco = promisify(retornaAlgoDoBanco);

promessaDeAlgoDoBanco({ id: 1 }, true)
    .then(res => console.log('restultado do banco', res))
    .catch(err => console.log('erro de acesso ao banco', err));

A função promisify assume que seu último argumento é um callback naquele modelo callback(erro, resultado). Uma implementação para o promisify um tanto ingênua seria o código abaixo. Aqui tem um polyfill para versões antes de node 8 que seria mais genérica e confiável.

Note a assinatura desta primeira linha, que é uma função que retorna outra função (que receberia os argumentos da função que usa callback). Por fim, esta função “de dentro” retorna uma nova promise.

const promisify = (funcaoLegado) => (...args) => new Promise(
    (resolve, reject) => funcaoLegado(args, (res, err) => {
        if (err) reject(err);
        else resolve(res);
    })
);

O uso de promessas “thenable” (que possuem o método then()) permite o encadeamento, desta maneira:

retornaAlgoDoBanco({
    id: 1
}, true)
    .then(dados => outraFuncDessaVezUmaPromise(dados))
    .then(dados => usa(dados))
    .then(dados => retornaAlgoDoBanco({
            id: 2
        }, true))
    .then(maisDadosAinda => usa(maisDadosAinda))
    .catch(err => /* trata o erro uma vez para qualquer parte do encadeamento */);
fazAlgumaOutraCoisa();

Agora ainda fica uma questão. Mesmo que seja uma arrow function bonitinha, nós ainda estamos usando callbacks, mesmo que seja de uma maneira mais padronizada. A linha em que eu coloquei o fazAlgumaOutraCoisa() não vai acontecer depois das promessas serem cumpridas (ou rejeitadas). Ou não. O código é assíncrono. Para solucionar este problema, foi criado um açúcar sintático: as keywords async/await.

# Async/Await

async é um açúcar sintático para declarar uma função assíncrona que retorna uma promise implicitamente. E await é um açúcar sintático para esperar o resultado de uma promise. Em caso de rejeição, um erro é lançado e será necessário usar o bloco try/catch. E é só isso. Isso resolve a pirâmide da desgraça de uma vez por todas. Vamos aos exemplos, baseados nos exemplos anteriores:

try {
    const promessaDeAlgoDoBanco = promisify(retornaAlgoDoBanco);
    const resultado = await promessaDeAlgoDoBanco({ id: 1 }, true);
} catch(err) {
    console.log('erro de acesso ao banco', err);
}

Ou se fosse o caso de implementar o acesso ao banco diretamente como uma função assíncrona:

const retornaAlgoDoBanco = async (parametros, booleanAleatorio) => {
    // chama o banco usando os parametros e o booleanAleatorio
    // caso dê erro
    throw new Error('erro de acesso ao banco');
    // caso de sucesso, o erro é nulo e os dados são "retornados" no segundo argumento
    return dados;
}

Inclusive, este código é equivalente ao anterior, podendo ser usado com a função then:

retornaAlgoDoBanco(parametros, false)
    .then(resultado => console.log(resultado))
    .catch(/* ... */);

Ao afirmar que uma função assíncrona retorna uma Promise de maneira implícita, significa que o interpretador do Javascript é bem espertinho e vai resolver as promises que forem possíveis de serem resolvidas antes da execução. O que torna o seguinte código redundante:

const assincrono = async () => {
    // ...
    return await dados;
}

Bem, era isso. Espero ter cumprido minha promessa (;