Front End

27 jun, 2017

Entenda tudo sobre Async/Await

Publicidade

Async/Await é uma das novas funcionalidades do ES2017. Com ela, é possível escrever código assíncrono como se estivéssemos escrevendo código síncrono. Essa funcionalidade já está disponível a partir da versão 7.6 do Node.js.

Neste artigo, pretendo demonstrar todas as possibilidades que aprendi até agora para trabalhar com async/await. Para todos os exemplos abaixo, vou utilizar a API do Star Wars: https://swapi.co/. A API tem endpoints para filmes, personagens, planetas, espécies, veículos e naves. Todos os exemplos abaixo estão disponíveis no meu GitHub.

Escrevendo uma função assíncrona com Promises

Antes de introduzir o assunto principal, vamos ver um exemplo de função assíncrona utilizando Promises.

const fetch = require('node-fetch');

function getPerson(id) {
  fetch(`http://swapi.co/api/people/${id}`)
    .then(response => response.json())
    .then(person => console.log(person.name));
}

getPerson(1);

No código acima, a função getPerson() faz uma chamada na API, processa o resultado e exibe o nome do personagem. Esse é um cenário bem comum encontrado hoje em dia.

Executando o código, temos o seguinte resultado:

$ node sample.js
Luke Skywalker

Escrevendo uma função assíncrona com async/await

Agora iremos escrever a mesma função, mas utilizando async/await.

const fetch = require('node-fetch');

async function getPerson(id) {
  const response = await fetch(`http://swapi.co/api/people/${id}`);
  const person = await response.json();
  console.log(person.name);
}

getPerson(1);

O primeiro passo é converter a declaração function para async function. Desta forma, estamos definindo que esta função será assíncrona.

// Promise
function getPerson(id) {...}

// async/await
async function getPerson(id) {...}

O próximo passo é utilizar await para cada processamento assíncrono dentro da função.

// Promise
fetch(`http://swapi.co/api/people/${id}`)
  .then(response => response.json())
  .then(person => console.log(person.name));

// async/await
const response = await fetch(`http://swapi.co/api/people/${id}`);
const person = await response.json();
console.log(person.name);

A própria leitura/interpretação do código fica mais fácil utilizando async/await. É como se estivéssemos programando de forma síncrona.

Executando o código, ainda temos o mesmo resultado.

$ node sample.js
Luke Skywalker

Utilizando async/await com Promises

Podemos combinar os dois mundos e utilizar async/await junto com Promises.

const fetch = require('node-fetch');

async function getPerson(id) {
  const response = await fetch(`http://swapi.co/api/people/${id}`);
  const person = await response.json();
  return person;
}

getPerson(1)
  .then(person => console.log(person.name));

Funções assíncronas sempre retornam Promises.

Nesse exemplo, estamos retornando o objeto person da função assíncrona e utilizando Promises para exibir o resultado no Console, pois o retorno da função é uma Promise.

Executando o código, ainda temos o mesmo resultado:

$ node sample.js
Luke Skywalker

No exemplo acima, estamos armazenando o retorno da chamada response.json() na variável person e retornando-a. Podemos simplificar e retornar diretamente o resultado da chamada response.json().

async function getPerson(id) {
  const response = await fetch(`http://swapi.co/api/people/${id}`);
  return await response.json();
}

Executando o código, ainda temos o mesmo resultado.

$ node sample.js
Luke Skywalker

Resolvendo ou rejeitando uma Promise com async/await

Para resolver uma Promise com async/await:

async function getPerson(id) {
  return id;
}

getPerson(1)
  .then(id => console.log(id)); // 1

Para rejeitar uma Promise com async/await:

async function getPerson(id) {
  throw Error('Not found');
}

getPerson(0)
  .catch(err => console.error(err.message)); // Not found

Tratamento de erros com Throw Error()

Uma maneira de tratar erros com funções assíncronas é utilizando throw Error(). O código abaixo irá tentar recuperar um personagem com id = 0.

const fetch = require('node-fetch');

async function getPerson(id) {
  const response = await fetch(`http://swapi.co/api/people/${id}`);
  return await response.json();
}

// id = 0
getPerson(0)
  .then(person => console.log(person.name));

Executando o código, temos o seguinte resultado.

$ node sample.js
undefined

Recebemos undefined, pois como não existe nenhum personagem com id = 0, a propriedade name não será preenchida.

Alterando a saída do console de person.name para person podemos entender o que está acontecendo.

...

// id = 0
getPerson(0)
  .then(person => console.log(person));

Executando o código, temos o seguinte resultado:

$ node sample.js
{ detail: 'Not found' }

Agora ficou mais claro o motivo de termos recebido undefined anteriormente. O objeto person possui apenas a propriedade detail com o valor “Not found”.

Nesse caso, podemos utilizar o throw Error() para tratar esse erro.

const fetch = require('node-fetch');

async function getPerson(id) {
  const response = await fetch(`http://swapi.co/api/people/${id}`);
  const body = await response.json();

  if (response.status !== 200) {
    throw Error(body.detail);
  }

  return body;
}

Armazenamos o retorno da chamada response.json() na variável body e testamos o status do response. Se o status for diferente de 200 (OK), disparamos um erro com o conteúdo de body.detail, caso contrário, retornamos o body.

Na primeira execução, passaremos o valor correto. O status do response será igual à 200 (OK) e o body será retornado, exibindo o nome do personagem.

getPerson(1)
  .then(person => console.log(person.name)) //Luke Skywalker
  .catch(err => console.error(err.message));

Na segunda execução, passaremos o valor errado. O status do response será diferente de 200 (OK) e um erro será disparado com o conteúdo de body.detail. Esse erro será capturado no catch e a mensagem “Not found” será exibida.

getPerson(0)
  .then(person => console.log(person.name))
  .catch(err => console.error(err.message)); // Not found

Tratamento de erros com try/catch

Outra alternativa para tratar erros com funções assíncronas é utilizando try/catch:

const fetch = require('node-fetch');

async function getPerson(id) {...}

async function loadPerson(id) {
  try {
    const person = await getPerson(id);
    console.log(person.name);

  } catch (err) {
    console.error(err.message);
  }
}

loadPerson(0);
loadPerson(1);

No exemplo acima, a função getPerson() permanece a mesma e introduzimos uma nova função assíncrona loadPerson() que fará o tratamento de erro.

Executando o código, temos o seguinte resultado:

$ node sample.js
Not found
Luke Skywalker

Utilizando async/await com function expressions

Neste exemplo, estamos atribuindo a função assíncrona para a variável getPerson. A chamada para a função é a mesma e o resultado também.

const fetch = require('node-fetch');

// regular function
const getPerson = async function (id) {
  const response = await fetch(`http://swapi.co/api/people/${id}`);
  return await response.json();
};

getPerson(1)
  .then(person => console.log(person.name));

Executando o código, temos o seguinte resultado:

$ node sample.js
Luke Skywalker

Podemos utilizar arrow functions também.

const fetch = require('node-fetch');

// arrow function
const getPerson = async (id) => {
  const response = await fetch(`http://swapi.co/api/people/${id}`);
  return await response.json();
};

getPerson(1)
  .then(person => console.log(person.name));

Executando o código, ainda temos o mesmo resultado:

$ node sample.js
Luke Skywalker

Entendendo o await

Nesse exemplo, tentaremos utilizar o await fora do escopo de uma função com async.

const fetch = require('node-fetch');

const getPerson = async (id) => {
  const response = await fetch(`http://swapi.co/api/people/${id}`);
  return await response.json();
};

const person = await getPerson(1);
console.log(person.name);

Executando o código, temos o seguinte resultado:

$ node sample.js

const person = await getPerson(1);
                     ^^^^^^^

SyntaxError: Unexpected identifier

Await só pode ser utilizado dentro de funções com Async.

O resultado é o erro acima, pois só podemos utilizar o await dentro do escopo de funções com async. No caso acima poderíamos ter utilizado Promise no lugar do await.

IIFE – Immediately-Invoked Function Expression

IIFE são funções que são invocadas imediatamente após a execução do programa. Para refrescar sua memória, podemos utilizar os seguintes exemplos:

  • Utilizando jQuery:
$("document").ready(function () {
  // code
});
  • Utilizando JavaScript
(function () {
  // code
})();

A IIFE de funções assíncronas é idêntica ao JavaScript, adicionando async na frente de function.

(async function () {
  // code
})();

Podemos utilizar arrow functions também:

(async () => {
  // code
})();

Como só podemos utilizar await dentro do escopo de funções com async, para resolver o caso acima, precisamos utilizar IIFE – Immediately-Invoked Function Expression.

const fetch = require('node-fetch');

const getPerson = async (id) => {
  const response = await fetch(`http://swapi.co/api/people/${id}`);
  return await response.json();
};

// IIFE - Immediately-Invoked Function Expression
(async function () {
  const person = await getPerson(1);
  console.log(person.name);
})();

Executando o código, ainda temos o mesmo resultado:

$ node sample.js
Luke Skywalker

Utilizando async/await com classes

É muito simples utilizar async/await com classes, basta adicionar async no início das funções. Neste exemplo, criamos uma classe chamada StarWars e definimos a função assíncrona getPerson(). Dentro do IIFE, instanciamos a classe StarWars e utilizamos o await.

const fetch = require('node-fetch');

// Class
class StarWars {
  async getPerson(id) {
    const response = await fetch(`http://swapi.co/api/people/${id}`);
    return await response.json();
  }
}

// IIFE - Immediately-Invoked Function Expression
(async () => {
  const sw = new StarWars();
  const person = await sw.getPerson(1);
  console.log(person.name);
})();

Executando o código, ainda temos o mesmo resultado.

$ node sample.js
Luke Skywalker

Exportando uma classe com funções assíncronas

Para os 03 próximos exemplos, vamos escrever a classe StarWars em um arquivo separado e vamos adicionar uma nova função para entender como trabalhar com múltiplas requisições.

// star-wars.js

const fetch = require('node-fetch');

class StarWars {
  async getPerson(id) {
    const response = await fetch(`http://swapi.co/api/people/${id}`);
    return await response.json();
  }

  async getFilm(id) {
    const response = await fetch(`http://swapi.co/api/films/${id}`);
    return await response.json();
  }
}

module.exports = StarWars;

No código acima, criamos e exportamos a classe StarWars com duas funções assíncronas: getPerson() e getFilm().

Aguardando múltiplas requisições em sequência

Podemos utilizar async/await para trabalhar com múltiplas requisições em sequência:

const StarWars = require('./star-wars');

async function loadData() {
  const sw = new StarWars();

  const person = await sw.getPerson(1);
  const film = await sw.getFilm(1);

  console.log(person.name);
  console.log(film.title);
}

loadData();

Neste exemplo, loadData() chama a função getPerson() e aguarda o resultado. Após o retorno do resultado, a função chama getFilm() e aguarda o resultado. Só após o segundo resultado é que os dados serão exibidos. Se cada uma das funções, getPerson() e getFilm(), demorassem 01 segundo para responder, os dados seriam exibidos após 02 segundos.

Executando o código, temos o seguinte resultado:

$ node sample.js
Luke Skywalker
A New Hope

Aguardando múltiplas requisições simultâneas

Podemos utilizar async/await para trabalhar com múltiplas requisições ao mesmo tempo.

const StarWars = require('./star-wars');

async function loadData() {
  const sw = new StarWars();

  const personPromise = sw.getPerson(1);
  const filmPromise = sw.getFilm(1);

  const person = await personPromise;
  const film = await filmPromise;

  console.log(person.name);
  console.log(film.title);
}

loadData();

Neste exemplo, loadData() chama simultaneamente as funções getPerson() e getFilm() e armazena as chamadas respectivamente em personPromise e filmPromise. Reparem que não foi utilizado await nas chamadas dessas funções, ele foi utilizado apenas no retorno dos resultados para as variáveis person e film. Os dados só serão exibidos quando as duas chamadas retornarem. Se cada uma das funções, getPerson() e getFilm(), demorassem 01 segundo para responder, os dados seriam exibidos após 01 segundo.

Executando o código, ainda temos o mesmo resultado:

$ node sample.js
Luke Skywalker
A New Hope

Aguardando múltiplas requisições simultâneas usando Promise.all()

Neste último exemplo, veremos uma forma mais simples e mais elegante de aguardar múltiplas requisições simultâneas.

const StarWars = require('./star-wars');

async function loadData() {
  const sw = new StarWars();

  const [person, film] = await Promise.all([sw.getPerson(1), sw.getFilm(1)]);

  console.log(person.name);
  console.log(film.title);
}

loadData();

O código acima aguarda o retorno das duas funções simultaneamente utilizando Promise.all(). Os resultados das funções getPerson() e getFilm() serão armazenados respectivamente em person e film utilizando destructuring, uma nova funcionalidade do ES6. Os dados só serão exibidos após o retorno das duas funções.

Executando o código, ainda temos o mesmo resultado.

$ node sample.js
Luke Skywalker
A New Hope

Conclusão

Essa nova funcionalidade traz inúmeros benefícios quando trabalhamos com funções assíncronas. Espero ter conseguido esclarecer todas as dúvidas com relação à async/await. Caso conheça algum outro cenário onde possamos utilizar async/await, deixe nos comentários.

Vamos em frente!

“Talk is cheap. Show me the code.” – Linus Torvalds