Javascript Callbacks, Promises e Async/Await

Entendendo as diferenças

Introdução

Algo relativamente comum na vida de qualquer programador javascript pode acabar gerando certa confusão em programadores iniciantes. O uso de Callbacks, Promises e Async/Await.

Aqui vai um post explicando o que é cada uma dessas features e suas diferentes formas de utilização.

O que é um callback

Uma função callback é uma função passada a outra função como argumento, que será chamada sempre que alguma rotina ou ação estiver completa.

Exemplo:

function greeting(name) {
    console.log('Hello ' + name);
}

function processOutput(callback) {
    callback('Jhony');
}

processOutput(greeting);

O exemplo acima é de um synchronous callback, como é executado imediatamente. No entanto funções de callback são comumente usadas em operações assíncronas.

Exemplo:

const fs = require('fs');

console.log(1);

fs.readFile('./in1.txt', (err, contents) => {
    console.log(err, String(contents));
});

console.log(2);

console.log(3);

O output do exemplo acima é:

1
2
3
null 'Eu sou in 1'

Vejamos que o código executou todos os logs e por último o fs.readFile, mesmo a função fs.readFile estar sendo chamada entre os logs 1 e 2.

Isso acontece porque o método readFile é assíncrono e só temos o seu output depois que ele é processado e invoca o callback que recebeu como segundo argumento, após concluir sua rotina de leitura do arquivo in1.txt. Dessa maneira o Javascript não bloqueia a Thread principal do programa enquanto executa métodos assíncronos.

O problema desse código pode aparecer a medida que precisamos ler mais arquivos, ou chamar mais funções assíncronas, e retornar essas operações de uma só vez, e gerenciar esse código aninhado pode ser um problema.

Exemplo:

fs.readFile('./in1.txt', (err, contents) => {
    fs.readFile('./in2.txt', (err2, contents2) => {
        fs.readFile('./in3.txt', (err3, contents3) => {
            fs.readFile('./in4.txt', (err4, contents4) => {
                console.log(err, String(contents));
                console.log(err2, String(contents2));
                console.log(err3, String(contents3));
                console.log(err4, String(contents4));
            });
        });
    });
});

Uma outra forma de resolver chamadas assíncronas são as famosas Promises.

O que é uma Promise?

Uma Promise é um objeto que representa a eventual conclusão ou falha de uma operação assíncrona. Essencialmente, uma Promise é um objeto retornado para o qual você adiciona callbacks, em vez de passar callbacks para uma função.

Exemplo:

const fs = require('fs');

const readFile = (file) => new Promise((resolve, reject) => {
    fs.readFile(file, (err, contents) => {
        if (err) {
            reject(err)
        } else {
            resolve(contents);
        }
    });
});

console.log(1);

readFile('./in1.txt')
    .then((contents) => {
        console.log(String(contents))
    });

console.log(2);

console.log(3);

O output do exemplo acima é:

1
2
3
Eu sou in 1

O exemplo acima traz uma facilidade maior de manutenção, pois dessa forma o nosso código cresce pra baixo e não pra frente, criando aquela "barriga" callbacks aninhados.

Outro ponto importante é que ao fazermos const readFile = (file) => new Promise((resolve, reject) a variável readFile recebe um valor imediato(Promise { <pending> }) que pode ou não ser resolvido posteriormente através do then, catch ou finally.

E pra fechar o artigo teremos a terceira maneira de resolver chamadas assíncronas, o elegante async/await ♥

O que é o async await?

Já sabemos que quando uma função assíncrona é chamada, ela retorna uma Promise. Com isso, uma função assíncrona pode conter uma expressão await, que pausa a execução da função assíncrona e espera pela resolução da Promise passada, e depois retoma a execução da função assíncrona e retorna o valor resolvido. Ou seja, o async/await nada mais é que uma forma mais "bonita" de resolver as Promises, dando a aparência de síncrono para um código assíncrono.

const fs = require('fs');

const readFile = (file) => new Promise((resolve, reject) => {
    fs.readFile(file, (err, contents) => {
        if (err) {
            reject(err)
        } else {
            resolve(contents);
        }
    });
});

console.log(1);

const init = async () => {
    const content = await readFile('./in1.txt');
    const content2 = await readFile('./in2.txt');
    console.log(String(content), String(content2));
}

console.log(init());

console.log(2);

console.log(3);

O output do exemplo acima é:

1
Promise { <pending> }
2
3
Eu sou in 1 Eu sou in 2

No exemplo acima a função init executa a função readFile que retorna uma Promise passando async na declaração da função e await na execução do readFile, aguardando a resolução da Promise para então mostrar o output console.log(String(content), String(content2)). Uma forma de tratar exceções no async/await é envolver as chamadas sobre o try/catch, assim quando uma Promise falhar o código irá lançar uma exceção.

Exemplo:

const init = async () => {
    try {
        const content = await readFile('./in1.txt');
        const content2 = await readFile('./in2.txt');
        console.log(String(content), String(content2));
    } catch (err) {
        console.log(err);
    }
}

Conclusão

Qualquer desenvolvedor Javascript vai precisar trabalhar com operações assíncronas em algum momento. E como vimos no artigo existem 3 formas de se trabalhar com esse tipo de operação, sendo elas:

Você pode conferir o código produzido durante a escrita do artigo no meu Github.

Dicas, sugestões, conselhos ou melhorias? Você pode entrar em contato comigo pelo meu email ou abrir uma PR no repositório aqui.

Comentários