DEV Community 👩‍💻👨‍💻

Cover image for Programação assíncrona
thaisandre
thaisandre

Posted on • Updated on

Programação assíncrona

Quando fazemos uma ligação telefônica para uma pessoa para passar uma mensagem, dependemos de outra ação que é a da pessoa atender a chamada. Vamos tentar representar isso em código utilizando a linguagem JavaScript:

function ligacao() {
    console.log("eu faço a chamada");
    console.log("a pessoa atende e diz alô");
    console.log("eu digo alguma informação"); 
}

ligacao();
Enter fullscreen mode Exit fullscreen mode

A saída será:

eu faço a chamada
a pessoa atende e diz alô
eu digo alguma informação
Enter fullscreen mode Exit fullscreen mode

Callbacks

Na realidade, a pessoa não atende a mensagem imediatamente, ela pode demorar alguns segundos para atender. Podemos representar essa "demora" através da função setTimeout que executa uma função após determinado período de tempo. Ela recebe dois argumentos - o primeiro é a função que representa a ação a ser executada e o segundo o valor em milisegundos representando o tempo mínimo de espera para que ela seja executada:

setTimeout(() => {
    console.log("a pessoa atende e diz alô")
}, 3000);
Enter fullscreen mode Exit fullscreen mode

Como resultado, após 3 segundos, temos:

a pessoa atende e diz alô
Enter fullscreen mode Exit fullscreen mode

Agora vamos utilizar este recurso no nosso exemplo:

function ligacao() {
    console.log("eu faço a chamada");
    setTimeout(() => {
        console.log("a pessoa atende e diz alô")
    }, 3000);
    console.log("eu digo alguma informação"); 
}
Enter fullscreen mode Exit fullscreen mode

saída:

eu faço a chamada
eu digo alguma informação
a pessoa atende e diz alô
Enter fullscreen mode Exit fullscreen mode

Note que nosso programa apresenta um problema: a pessoa que fez a chamada (no caso, eu) acaba dizendo alguma coisa antes da outra pessoa atender. Ou seja, a execução não aconteceu de maneira síncrona, mantendo a ordenação esperada. O conteúdo dentro de setTimeout não foi executado imediatamente após a primeira chamada de console.log.

O JavaScript é single-threaded. O que quer dizer, grosso modo, que possui uma stack principal de execução do programa e executa um comando por vez, do início ao fim, sem interrupções. No momento em que cada operação é processada, nada mais pode acontecer.

Acabamos de ver que o funcionamento de nosso programa é diferente quando encontra a função setTimeout. No Node.js, o método setTimeout pertence ao módulo timers que contém funções que executam algum código após um determinado período de tempo. Não é necessário importar este módulo no Node.js já que todos estes métodos estão disponíveis globalmente para simular o JavaScript Runtime Environment dos navegadores.

A chamada da função que passamos como primeiro argumento para o setTimeout é enviada para outro contexto, chamado WEBApi que define um timer com o valor que passamos como segundo argumento (3000) e aguarda este tempo para colocar a chamada da função na stack principal para ser executada - ocorre um agendamento desta execução. Porém, este agendamento só é concretizado após a stack principal ser limpa, ou seja, após todo código síncrono ser executado. Por este motivo, a terceira e última chamada de console.log é chamada antes da segunda.

A função que passamos como primeiro argumento para o método setTimeout é chamada de função callback. Uma função callback é toda função passada como argumento para outra função que de fato vai executá-la. Esta execução pode ser imediata, ou seja, executada de maneira síncrona. No entanto, callbacks são normalmente utilizados para continuar a execução de um código em outro momento na linha do tempo, ou seja, de maneira assíncrona. Isso é bastante útil quando temos eventos demorados e não queremos travar o restante do programa.

Nosso código ainda tem problemas. A pessoa que faz a ligação quer apenas dizer alguma coisa após a outra pessoa atender a chamada. Podemos refatorar o código da seguinte maneira:

function fazChamada(){
    console.log("eu faço a chamada");
}

function pessoaAtende() {
    setTimeout(() => {
        console.log("a pessoa atende e diz alô")
    }, 3000);
}

function euDigoAlgo() {
    setTimeout(() => {
        console.log("eu digo alguma informação");
    }, 5000); // tempo de espera maior 
}

function ligacao() {
    fazChamada();
    pessoaAtende();
    euDigoAlgo();
}

ligacao();
Enter fullscreen mode Exit fullscreen mode

Podemos definir um tempo de espera maior para dizer algo na chamada, mas ainda assim não sabemos ao certo o quanto a pessoa vai demorar para atender. Se ela atender imediatamente, vai demorar para receber a mensagem e desligar a chamada sem que isso aconteça. Além de ser bastante ruim e trabalhoso ficar configurando os tempos de cada execução, o código fica muito grande e confuso com muitas condicionais.

Promises

Para nossa sorte, o JavaScript possui um recurso chamado Promise que representa, como seu nome sugere, uma promessa de algo que será executado futuramente. Como a execução que esperamos pode falhar, este recurso também ajuda muito nos tratamentos de erros.

Segundo a Wikipédia, um Promise atua como representante de um resultado que é, inicialmente, desconhecido devido a sua computação não estar completa no momento de sua chamada. Vamos construir um objeto Promise para entender seu funcionamento:

const p = new Promise();
console.log(p);
Enter fullscreen mode Exit fullscreen mode

Isso vai gerar um TypeError com a mensagem "TypeError: Promise resolver is not a function". Um objeto Promise precisa receber uma função para resolver um valor. Ou seja, precisamos passar uma função callback para executar algo:

const p = new Promise(() => console.log(5));
Enter fullscreen mode Exit fullscreen mode

Este código imprime o valor 5. Agora vamos imprimir o próprio objeto Promise:

const p = new Promise(() => console.log(5));
console.log(p);
Enter fullscreen mode Exit fullscreen mode

Saída:

5
Promise { <pending> }
Enter fullscreen mode Exit fullscreen mode

Note que o callback foi executado, mas seu estado está pendente. Toda vez que criamos um objeto Promise, seu estado inicial é pendente já que representa a promessa de algo que será resolvido no futuro. Neste caso, como o callback será executado de maneira síncrona, vai imprimir o resultado de sua execução. E, portanto, não é útil neste caso específico.

Pode acontecer do callback executar o processamento de um valor que será necessário no futuro. Para que este valor esteja disponível, será preciso que a promessa seja resolvida através da função anônima resolve que cria uma nova promessa com o valor realizado. Exemplo:

const p = new Promise((resolve) => {
    resolve(5);
});
console.log(p);
Enter fullscreen mode Exit fullscreen mode

Saída:

Promise { 5 }
Enter fullscreen mode Exit fullscreen mode

Agora a promessa não está mais pendente, ela foi resolvida e embrulha o valor 5. Isso quer dizer que tudo deu certo. Porém, ainda é uma promessa. Para imprimir o valor, precisamos utilizar o método then que anexa callbacks para a resolução:

const p = new Promise((resolve) => {
    resolve(5);
});
p.then(value => console.log(value));
Enter fullscreen mode Exit fullscreen mode

Saída:

5
Enter fullscreen mode Exit fullscreen mode

Mas um erro pode acontecer quando a promessa tentar resolver um valor:

const p = new Promise((resolve) => {
    try {
        throw new Error("algo de errado ocorreu"); // um erro acontece
        resolve(5);
    } catch(err) {
        return err;
    }
});
console.log(p);
p.then(v => console.log(v))
Enter fullscreen mode Exit fullscreen mode

Saída:

Promise { <pending> }
Enter fullscreen mode Exit fullscreen mode

A promessa está pendente, mas nada foi executado ao chamarmos then(v => console.log(v)) porque um erro aconteceu antes que a promessa fosse resolvida. Para sabermos qual erro ocorreu, precisamos passar outro callback que será responsável por tratar falhas quando a promessa de um resultado for rejeitada, chamado reject.

const p = new Promise((resolve, reject) => {
    try {
        throw new Error("algo de errado ocorreu");
        resolve(5);
    } catch(err) {
        reject(err);  // chamada de reject
    }
});
console.log(p);
Enter fullscreen mode Exit fullscreen mode

Saída:

Promise {
  <rejected> Error: algo de errado ocorreu
      at /home/caelum/Documents/estudos/js/exercicios/promise.js:58:15
      at new Promise (<anonymous>)
      at Object.<anonymous> (/home/caelum/Documents/estudos/js/exercicios/promise.js:56:11)
      at Module._compile (internal/modules/cjs/loader.js:1063:30)
      at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
      at Module.load (internal/modules/cjs/loader.js:928:32)
      at Function.Module._load (internal/modules/cjs/loader.js:769:14)
      at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)
      at internal/main/run_main_module.js:17:47
}
(node:14346) UnhandledPromiseRejectionWarning: Error: algo de errado ocorreu
...
Enter fullscreen mode Exit fullscreen mode

O estado da promessa agora será rejected. Além do estado da promessa, o Node.js mostra um warning com a seguinte mensagem: "UnhandledPromiseRejectionWarning: Error: algo de errado ocorreu". Ou seja, a promessa rejeitada não foi tratada. Após a chamada de then, que só será executado em caso de sucesso, podemos chamar o catch que será chamado em caso de erro:

const p = new Promise((resolve, reject) => {
    try {
        throw new Error("algo de errado ocorreu");
        resolve(5);
    } catch(err) {
        reject(err);
    }
});
p.then(v => console.log(v)).catch(err => console.log(err.message));
//console.log(p);
Enter fullscreen mode Exit fullscreen mode

Saída:

algo de errado ocorreu
Enter fullscreen mode Exit fullscreen mode

A mensagem de erro será impressa na execução do catch.

Promises são bastante úteis para chamadas assíncronas, quando precisamos saber sobre os estados de execuções futuras e tratar melhor as partes do código que dependem dessas execuções.

Agora, vamos voltar ao nosso exemplo. Podemos utilizar Promises para melhorar o código e fazer com que a pessoa que fez a chamada diga algo após a outra pessoa atender a chamada:

function fazChamada(){
    console.log("eu faço a chamada");
}

function pessoaAtende() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            let atendeu = Math.random() > 0.5; 
            if(atendeu) {
                resolve("alô");
            } else {
                reject(new Error("a pessoa não atendeu")); 
            }
        }, 3000);

    });
}

function pessoaDiz(msg) {
    console.log(`a pessoa atende e diz ${msg}`);
}

function euDigoAlgo() {
    console.log("eu digo alguma informação");
}

function ligacao() {
    fazChamada();
    pessoaAtende()
        .then((msg) => pessoaDiz(msg))
        .then(euDigoAlgo)
        .catch(err => console.log(err.message));
}

ligacao();
Enter fullscreen mode Exit fullscreen mode

Para deixar o código mais realista, adicionamos a linha let atendeu = Math.random() > 0.5; para representar se a pessoa atendeu ou não. E tratamos o caso em que ela não atende como uma falha na ligação.

No caso da pessoa atender, teremos a saída:

eu faço a chamada
a pessoa atende e diz alô
eu digo alguma informação
Enter fullscreen mode Exit fullscreen mode

Caso ela não atenda, a saída será:

eu faço a chamada
a pessoa não atendeu
Enter fullscreen mode Exit fullscreen mode

Async/Await

Nosso código funciona e conseguimos representar uma chamada telefônica mais próxima da realidade. Porém, o código da função ligacao() possui uma chamada encadeada de várias promessas - e poderia ser muito mais complexo do que isso, como muitas chamadas encadeadas de then(). Dependendo da complexidade dessas chamadas, pode ser um código difícil de ler e entender. Um código síncrono é, na maioria dos casos, mais fácil de ler e entender.

Na especificação ES2017 foram introduzidas duas novas expressões - async e await - que deixam o trabalho com Promises mais confortável para o desenvolvedor. A expressão async é utilizada quando queremos criar funções assíncronas. Quando posicionada antes da declaração de uma função, quer dizer que essa função retorna um objeto do tipo Promise:

async function retornaUm() {
    return 1;
}
console.log(retornaUm());
retornaUm().then(console.log);
Enter fullscreen mode Exit fullscreen mode

Que vai gerar a saída:

Promise { 1 }
1
Enter fullscreen mode Exit fullscreen mode

Portanto, ao utilizar a expressão async em uma função, seu retorno é embrulhado em um objeto Promise. Agora que entendemos como funciona o async vamos ver como o await funciona.

O uso do await somente é permitido em escopo de um função async - deste modo, a palavra-chave async além de embrulhar seu retorno em uma promessa, permite o uso do await. A palavra-chave await faz com que o JavaScript espere até que uma promessa seja resolvida (ou rejeitada) e retorne seu resultado.

async function retornaUm() {
    return 1;
}

async function retornaDois() {
    var num = await retornaUm();
    return num + 1;
}

retornaDois().then(console.log)
Enter fullscreen mode Exit fullscreen mode

Saída:

2
Enter fullscreen mode Exit fullscreen mode

A função retornaDois espera a promessa retonraUm ser resolvida para seguir sua execução. Portanto, espera a promessa ser finalizada. O mesmo acontece quando o valor é rejeitado:

async function funcao() {
    await Promise.reject(new Error("um erro ocorreu"));
}

funcao().catch(err => console.log(err.message));
Enter fullscreen mode Exit fullscreen mode

Saída:

um erro ocorreu
Enter fullscreen mode Exit fullscreen mode

E é similar a:

async function funcao() {
    await new Error("um erro ocorreu");
}

funcao().catch(err => console.log(err.message));
Enter fullscreen mode Exit fullscreen mode

Saída:

um erro ocorreu
Enter fullscreen mode Exit fullscreen mode

Como o código posicionado após o await lança um erro, podemos fazer um tratamento com o bloco try/catch:

async function funcao() {
    try {
        await Promise.reject(new Error("um erro ocorreu"));
    } catch(err) {
        console.log(err.message);
    }
}

funcao();
Enter fullscreen mode Exit fullscreen mode

Note que o código fica mais fácil de ler e raramente usamos as chamadas encadeadas de then e catch. Com a introdução de funções assíncronas com async/await, a escrita de um código assíncrono fica parecido com a escrita de um código síncrono.

Agora que aprendemos como funciona o async/await, podemos refatorar nosso código para utilizar este recurso:

function fazChamada(){
    console.log("eu faço a chamada");
}

function pessoaAtende() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const atendeu = Math.random() > 0.5;
            if(atendeu) {
                resolve("alô");
            } else {
                reject(new Error("a pessoa nao atendeu")); 
            }
        }, 3000);
    });
}

function pessoaDiz(msg) {
    console.log(`a pessoa atende e diz ${msg}`);
}

function euDigoAlgo() {
    console.log("eu digo alguma informação");
}

async function ligacao() {
    fazChamada();
    try {
        const msg = await pessoaAtende();
        pessoaDiz(msg);
        euDigoAlgo();
    }catch(err) {
        console.log(err.message);
    }
}

ligacao();
Enter fullscreen mode Exit fullscreen mode

Top comments (0)

Classic DEV Post from 2020:

js visualized

🚀⚙️ JavaScript Visualized: the JavaScript Engine

As JavaScript devs, we usually don't have to deal with compilers ourselves. However, it's definitely good to know the basics of the JavaScript engine and see how it handles our human-friendly JS code, and turns it into something machines understand! 🥳

Happy coding!