Programação assíncrona
Tags: javascript, beginners
Published: 2021-05-11 01:35:30

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:

```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:

```javascript
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:

```javascript
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](https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide/In_depth) 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:

```javascript
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](https://en.wikipedia.org/wiki/Futures_and_promises), 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:

```javascript
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:

```javascript
const p = new Promise(() => console.log(5));
```

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

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
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`.

```javascript
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:

```javascript
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:

```javascript
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](https://262.ecma-international.org/8.0/) 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`:

```javascript
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.

```javascript
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:

```javascript
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:

```javascript
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`:

```javascript
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:

```javascript
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();
```