Programação assíncrona
thaisandre
Posted on May 11, 2021
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();
A saída será:
eu faço a chamada
a pessoa atende e diz alô
eu digo alguma informação
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);
Como resultado, após 3 segundos, temos:
a pessoa atende e diz alô
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");
}
saída:
eu faço a chamada
eu digo alguma informação
a pessoa atende e diz alô
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();
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);
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));
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);
Saída:
5
Promise { <pending> }
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);
Saída:
Promise { 5 }
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));
Saída:
5
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))
Saída:
Promise { <pending> }
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);
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
...
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);
Saída:
algo de errado ocorreu
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();
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
Caso ela não atenda, a saída será:
eu faço a chamada
a pessoa não atendeu
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);
Que vai gerar a saída:
Promise { 1 }
1
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)
Saída:
2
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));
Saída:
um erro ocorreu
E é similar a:
async function funcao() {
await new Error("um erro ocorreu");
}
funcao().catch(err => console.log(err.message));
Saída:
um erro ocorreu
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();
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();
Posted on May 11, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.