Entendendo o Event Loop, Callbacks, Promises e Async/Await no JavaScript

O autor selecionou o Fundo de Auxílio COVID-19 para receber uma doação como parte do programa Escreva para Doações.

Introdução

Nos primeiros dias da internet, os sites frequentemente consistiam em dados estáticos em uma página HTML. Mas agora que as aplicações web se tornaram mais interativas e dinâmicas, tornou-se cada vez mais necessário realizar operações intensivas como fazer solicitações de rede externa para recuperar dados da API. Para lidar com essas operações em JavaScript, um desenvolvedor deve usar técnicas de programação assíncrona.

Como o JavaScript é uma linguagem de programação monotarefa com um modelo de execução síncrono que processa uma operação após a outra, só pode processar uma declaração de cada vez. No entanto, uma ação como solicitar dados de uma API pode levar um tempo indeterminado, dependendo do tamanho dos dados solicitados, da velocidade da conexão de rede e de outros fatores. Se as chamadas de API fossem realizadas de forma síncrona, o navegador não seria capaz de lidar com nenhuma entrada do usuário, como rolar ou clicar em um botão, até que essa operação fosse concluída. Isso é conhecido como bloqueio.

Para evitar comportamentos de bloqueio, o ambiente do navegador possui muitas APIs da Web que o JavaScript pode acessar de forma assíncrona, ou seja, elas podem ser executadas em paralelo com outras operações em vez de sequencialmente. Isso é útil porque permite que o usuário continue usando o navegador normalmente enquanto as operações assíncronas estão sendo processadas.

Como desenvolvedor JavaScript, você precisa saber como trabalhar com as APIs da Web assíncronas e lidar com a resposta ou erro dessas operações. Neste artigo, você aprenderá sobre o ciclo de eventos, a forma original de lidar com o comportamento assíncrono por meio de callbacks, a adição de promessas da especificação ECMAScript 2015 e a prática moderna de usar async/await.

Observação: Este artigo está focado em JavaScript do lado do cliente no ambiente do navegador. Os mesmos conceitos geralmente são válidos no ambiente do Node.js, no entanto, o Node.js usa suas próprias APIs em C++ em oposição às APIs da Web do navegador.

O Loop de Eventos

Esta seção explicará como o JavaScript lida com código assíncrono usando o loop de eventos. Primeiro, será feita uma demonstração do loop de eventos em funcionamento, e então serão explicados os dois elementos do loop de eventos: a pilha e a fila.

O código JavaScript que não utiliza nenhuma API da Web assíncrona executará de forma síncrona, um de cada vez, sequencialmente. Isso é demonstrado pelo código de exemplo a seguir, que chama três funções que imprimem um número no console:

// Definir três funções de exemplo
function first() {
  console.log(1)
}

function second() {
  console.log(2)
}

function third() {
  console.log(3)
}

Neste código, você define três funções que imprimem números com console.log().

Em seguida, escreva chamadas para as funções:

// Executar as funções
first()
second()
third()

A saída será baseada na ordem em que as funções foram chamadas — primeira(), segunda() e depois terceira():

Output
1 2 3

Quando uma API da Web assíncrona é usada, as regras se tornam mais complicadas. Uma API integrada que você pode testar isso é o setTimeout, que define um temporizador e realiza uma ação após um período especificado de tempo. setTimeout precisa ser assíncrono, caso contrário, todo o navegador permaneceria congelado durante a espera, o que resultaria em uma experiência ruim para o usuário.

Adicione setTimeout à função segunda para simular uma solicitação assíncrona:

// Definir três funções de exemplo, mas uma delas contém código assíncrono
function first() {
  console.log(1)
}

function second() {
  setTimeout(() => {
    console.log(2)
  }, 0)
}

function third() {
  console.log(3)
}

setTimeout recebe dois argumentos: a função que será executada de forma assíncrona e o tempo que irá esperar antes de chamar essa função. Neste código, você envolveu console.log em uma função anônima e a passou para setTimeout, então definiu a função para ser executada após 0 milissegundos.

Agora chame as funções, como fez anteriormente:

// Execute as funções
first()
second()
third()

Você pode esperar que com um setTimeout definido para 0 milissegundos, a execução dessas três funções ainda resultaria nos números sendo impressos em ordem sequencial. Mas como é assíncrono, a função com o timeout será impressa por último:

Output
1 3 2

Independentemente de definir o timeout para zero segundos ou cinco minutos, não fará diferença—o console.log chamado pelo código assíncrono será executado após as funções síncronas de nível superior. Isso acontece porque o ambiente host do JavaScript, neste caso o navegador, usa um conceito chamado de loop de eventos para lidar com concorrência, ou eventos paralelos. Como o JavaScript só pode executar uma instrução por vez, ele precisa que o loop de eventos seja informado de quando executar qual instrução específica. O loop de eventos trata isso com os conceitos de uma pilha e uma fila.

Pilha

A pilha, ou pilha de chamadas, mantém o estado de qual função está atualmente em execução. Se você não está familiarizado com o conceito de uma pilha, pode imaginá-la como um array com propriedades de “último a entrar, primeiro a sair” (LIFO), o que significa que você só pode adicionar ou remover itens do final da pilha. O JavaScript executará o quadro atual (ou chamada de função em um ambiente específico) na pilha e, em seguida, o removerá e passará para o próximo.

Para o exemplo que contém apenas código síncrono, o navegador manipula a execução na seguinte ordem:

  • Adicione first() à pilha, execute first() que registra 1 no console, remova first() da pilha.
  • Adicione second() à pilha, execute second() que registra 2 no console, remova second() da pilha.
  • Adicione third() à pilha, execute third() que registra 3 no console, remova third() da pilha.

O segundo exemplo com setTimout se parece com isso:

  • Adicione first() à pilha, execute first() que registra 1 no console, remova first() da pilha.
  • Adicione second() à pilha, execute second().
    • Adicione setTimeout() à pilha, execute o setTimeout() Web API que inicia um temporizador e adiciona a função anônima à fila, remova setTimeout() da pilha.
  • Remova second() da pilha.
  • Adicione third() à pilha, execute third() que registra 3 no console, remova third() da pilha.
  • O event loop verifica a fila em busca de mensagens pendentes e encontra a função anônima de setTimeout(), adiciona a função à pilha que registra 2 no console, em seguida, remove-a da pilha.

O uso de setTimeout, uma API da Web assíncrona, introduz o conceito da fila, que este tutorial abordará em seguida.

Fila

A fila, também chamada de fila de mensagens ou fila de tarefas, é uma área de espera para funções. Sempre que a pilha de chamadas estiver vazia, o event loop verificará a fila em busca de mensagens pendentes, começando pela mensagem mais antiga. Quando encontrar uma, adicionará à pilha, que executará a função na mensagem.

No exemplo de setTimeout, a função anônima é executada imediatamente após o restante da execução do nível superior, uma vez que o temporizador foi definido para 0 segundos. É importante lembrar que o temporizador não significa que o código será executado exatamente em 0 segundos ou no tempo especificado, mas sim que ele adicionará a função anônima à fila naquele período de tempo. Esse sistema de fila existe porque se o temporizador adicionasse a função anônima diretamente à pilha quando o temporizador terminasse, isso interromperia qualquer função que esteja sendo executada no momento, o que poderia ter efeitos indesejados e imprevisíveis.

Nota: Também existe outra fila chamada fila de tarefas ou fila de microtarefas que lida com promessas. Microtarefas como promessas são tratadas com prioridade mais alta do que tarefas de macro como setTimeout.

Agora você sabe como o loop de eventos usa a pilha e a fila para lidar com a ordem de execução do código. A próxima tarefa é descobrir como controlar a ordem de execução em seu código. Para fazer isso, você aprenderá primeiro sobre a maneira original de garantir que o código assíncrono seja tratado corretamente pelo loop de eventos: funções de retorno de chamada.

Funções de Retorno de Chamada

No exemplo de setTimeout, a função com o tempo de espera é executada após tudo no contexto de execução principal de nível superior. Mas se você quiser garantir que uma das funções, como a função third, seja executada após o tempo de espera, então você teria que usar métodos de codificação assíncrona. O tempo de espera aqui pode representar uma chamada de API assíncrona que contém dados. Você quer trabalhar com os dados da chamada de API, mas precisa garantir que os dados sejam retornados primeiro.

A solução original para lidar com esse problema é usar funções de retorno de chamada. As funções de retorno de chamada não têm uma sintaxe especial; são apenas uma função que foi passada como argumento para outra função. A função que recebe outra função como argumento é chamada de função de ordem superior. De acordo com essa definição, qualquer função pode se tornar uma função de retorno de chamada se for passada como argumento. As funções de retorno de chamada não são assíncronas por natureza, mas podem ser usadas para fins assíncronos.

Aqui está um exemplo de código sintático de uma função de ordem superior e uma função de retorno de chamada:

// Uma função
function fn() {
  console.log('Just a function')
}

// Uma função que recebe outra função como argumento
function higherOrderFunction(callback) {
  // Quando você chama uma função que é passada como argumento, ela é referida como uma função de retorno de chamada
  callback()
}

// Passando uma função
higherOrderFunction(fn)

Neste código, você define uma função fn, define uma função higherOrderFunction que recebe uma função callback como argumento e passa fn como uma função de retorno de chamada para higherOrderFunction.

Executar este código resultará no seguinte:

Output
Just a function

Vamos voltar para as funções first, second e third com setTimeout. Até agora, é isso que você tem:

function first() {
  console.log(1)
}

function second() {
  setTimeout(() => {
    console.log(2)
  }, 0)
}

function third() {
  console.log(3)
}

A tarefa é fazer com que a função third sempre atrase a execução até que a ação assíncrona na função second tenha sido concluída. É aqui que os callbacks entram. Em vez de executar first, second e third no nível superior de execução, você passará a função third como argumento para second. A função second executará o callback após a conclusão da ação assíncrona.

Aqui estão as três funções com um callback aplicado:

// Definir três funções
function first() {
  console.log(1)
}

function second(callback) {
  setTimeout(() => {
    console.log(2)

    // Executar a função de callback
    callback()
  }, 0)
}

function third() {
  console.log(3)
}

Agora, execute first e second, depois passe third como argumento para second:

first()
second(third)

Após executar este bloco de código, você receberá a seguinte saída:

Output
1 2 3

Primeiro 1 será impresso, e após o temporizador completar (neste caso, zero segundos, mas você pode alterá-lo para qualquer valor) ele imprimirá 2 e então 3. Ao passar uma função como callback, você conseguiu atrasar com sucesso a execução da função até que a API Web assíncrona (setTimeout) seja concluída.

O ponto principal aqui é que as funções de retorno de chamada não são assíncronas – setTimeout é a API da Web assíncrona responsável por lidar com tarefas assíncronas. A função de retorno de chamada apenas permite que você seja informado quando uma tarefa assíncrona for concluída e lida com o sucesso ou falha da tarefa.

Agora que você aprendeu como usar callbacks para lidar com tarefas assíncronas, a próxima seção explica os problemas de aninhar muitos callbacks e criar uma “pirâmide de dor”.

Callbacks Aninhados e a Pirâmide da Dor

Funções de retorno de chamada são uma maneira eficaz de garantir a execução retardada de uma função até que outra seja concluída e retorne com dados. No entanto, devido à natureza aninhada dos callbacks, o código pode acabar ficando bagunçado se você tiver muitas solicitações assíncronas consecutivas que dependem umas das outras. Isso foi uma grande frustração para os desenvolvedores JavaScript no início, e como resultado, o código contendo callbacks aninhados é frequentemente chamado de “pirâmide da dor” ou “inferno de callbacks”.

Aqui está uma demonstração de callbacks aninhados:

function pyramidOfDoom() {
  setTimeout(() => {
    console.log(1)
    setTimeout(() => {
      console.log(2)
      setTimeout(() => {
        console.log(3)
      }, 500)
    }, 2000)
  }, 1000)
}

Neste código, cada novo setTimeout está aninhado dentro de uma função de ordem superior, criando uma forma de pirâmide de callbacks cada vez mais profunda. Executar este código resultaria no seguinte:

Output
1 2 3

Na prática, com código assíncrono do mundo real, isso pode se tornar muito mais complicado. Você provavelmente precisará lidar com erros no código assíncrono e, em seguida, passar alguns dados de cada resposta para a próxima solicitação. Fazer isso com callbacks tornará seu código difícil de ler e manter.

Aqui está um exemplo executável de um “pirâmide do caos” mais realista com o qual você pode brincar:

// Função assíncrona de exemplo
function asynchronousRequest(args, callback) {
  // Lança um erro se nenhum argumento for passado
  if (!args) {
    return callback(new Error('Whoa! Something went wrong.'))
  } else {
    return setTimeout(
      // Apenas adicionando um número aleatório para parecer que a função assíncrona
      // retornou dados diferentes
      () => callback(null, {body: args + ' ' + Math.floor(Math.random() * 10)}),
      500,
    )
  }
}

// Solicitações assíncronas aninhadas
function callbackHell() {
  asynchronousRequest('First', function first(error, response) {
    if (error) {
      console.log(error)
      return
    }
    console.log(response.body)
    asynchronousRequest('Second', function second(error, response) {
      if (error) {
        console.log(error)
        return
      }
      console.log(response.body)
      asynchronousRequest(null, function third(error, response) {
        if (error) {
          console.log(error)
          return
        }
        console.log(response.body)
      })
    })
  })
}

// Executar
callbackHell()

Neste código, você deve fazer com que cada função considere uma possível resposta e um possível erro, tornando a função callbackHell visualmente confusa.

Executando este código, você obterá o seguinte:

Output
First 9 Second 3 Error: Whoa! Something went wrong. at asynchronousRequest (<anonymous>:4:21) at second (<anonymous>:29:7) at <anonymous>:9:13

Essa forma de lidar com código assíncrono é difícil de acompanhar. Como resultado, o conceito de promessas foi introduzido no ES6. Esse é o foco da próxima seção.

Promessas

A promise represents the completion of an asynchronous function. It is an object that might return a value in the future. It accomplishes the same basic goal as a callback function, but with many additional features and a more readable syntax. As a JavaScript developer, you will likely spend more time consuming promises than creating them, as it is usually asynchronous Web APIs that return a promise for the developer to consume. This tutorial will show you how to do both.

Criando uma Promessa

Você pode inicializar uma promessa com a sintaxe new Promise, e você deve inicializá-la com uma função. A função que é passada para uma promessa tem parâmetros resolve e reject. As funções resolve e reject lidam com o sucesso e o fracasso de uma operação, respectivamente.

Escreva a seguinte linha para declarar uma promessa:

// Inicializar uma promessa
const promise = new Promise((resolve, reject) => {})

Se você inspecionar a promessa inicializada neste estado com o console do seu navegador da web, você descobrirá que ela tem um status pendente e um valor indefinido:

Output
__proto__: Promise [[PromiseStatus]]: "pending" [[PromiseValue]]: undefined

Até agora, nada foi configurado para a promessa, então ela vai ficar lá em um estado pendente para sempre. A primeira coisa que você pode fazer para testar uma promessa é cumprir a promessa resolvendo-a com um valor:

const promise = new Promise((resolve, reject) => {
  resolve('We did it!')
})

Agora, ao inspecionar a promessa, você verá que ela tem um status de cumprida e um valor definido como o valor que você passou para resolve:

Output
__proto__: Promise [[PromiseStatus]]: "fulfilled" [[PromiseValue]]: "We did it!"

Conforme afirmado no início desta seção, uma promessa é um objeto que pode retornar um valor. Após ser cumprida com sucesso, o valor passa de indefinido para ser preenchido com dados.

A promise can have three possible states: pending, fulfilled, and rejected.

  • Pendente – Estado inicial antes de ser resolvida ou rejeitada
  • Realizado – Operação bem-sucedida, promessa foi resolvida
  • Rejeitado – Operação fracassada, promessa foi rejeitada

Após ser realizada ou rejeitada, uma promessa é resolvida.

Agora que você tem uma ideia de como as promessas são criadas, vamos ver como um desenvolvedor pode consumir essas promessas.

Consumindo uma Promessa

A promessa na última seção foi realizada com um valor, mas você também deseja ser capaz de acessar esse valor. As promessas têm um método chamado then que será executado depois que uma promessa alcançar o resolve no código. then irá retornar o valor da promessa como parâmetro.

É assim que você retornaria e registraria o valor da promessa de exemplo:

promise.then((response) => {
  console.log(response)
})

A promessa que você criou tinha um [[PromiseValue]] de Nós conseguimos!. Esse valor é o que será passado para a função anônima como resposta:

Output
We did it!

Até agora, o exemplo que você criou não envolveu uma API da Web assíncrona – apenas explicou como criar, resolver e consumir uma promessa JavaScript nativa. Usando setTimeout, você pode testar uma solicitação assíncrona.

O código a seguir simula dados retornados de uma solicitação assíncrona como uma promessa:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => resolve('Resolving an asynchronous request!'), 2000)
})

// Registrar o resultado
promise.then((response) => {
  console.log(response)
})

Usar a sintaxe then garante que a resposta será registrada somente quando a operação setTimeout for concluída após 2000 milissegundos. Tudo isso é feito sem aninhar callbacks.

Agora, após dois segundos, a promessa será resolvida e o valor será registrado em then:

Output
Resolving an asynchronous request!

As promessas também podem ser encadeadas para passar dados para mais de uma operação assíncrona. Se um valor for retornado em then, outro then pode ser adicionado que será concluído com o valor de retorno do then anterior:

// Encadear uma promessa
promise
  .then((firstResponse) => {
    // Retornar um novo valor para o próximo then
    return firstResponse + ' And chaining!'
  })
  .then((secondResponse) => {
    console.log(secondResponse)
  })

A resposta cumprida no segundo then registrará o valor de retorno:

Output
Resolving an asynchronous request! And chaining!

Como o then pode ser encadeado, permite que o consumo de promessas pareça mais síncrono do que callbacks, pois não precisam ser aninhados. Isso permitirá um código mais legível que pode ser mantido e verificado com mais facilidade.

Tratamento de Erros

Até agora, você apenas tratou uma promessa com um resolve bem-sucedido, que coloca a promessa em um estado fulfilled. Mas frequentemente, com uma solicitação assíncrona, você também precisa lidar com um erro – se a API estiver fora do ar, ou se uma solicitação malformada ou não autorizada for enviada. Uma promessa deve ser capaz de lidar com ambos os casos. Nesta seção, você criará uma função para testar tanto o caso de sucesso quanto o caso de erro de criação e consumo de uma promessa.

Esta função getUsers passará um sinalizador para uma promessa e retornará a promessa:

function getUsers(onSuccess) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // Lidar com resolve e reject na API assíncrona
    }, 1000)
  })
}

Configure o código para que, se onSuccess for true, o tempo limite seja concluído com alguns dados. Se false, a função será rejeitada com um erro:

function getUsers(onSuccess) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // Lidar com resolve e reject na API assíncrona
      if (onSuccess) {
        resolve([
          {id: 1, name: 'Jerry'},
          {id: 2, name: 'Elaine'},
          {id: 3, name: 'George'},
        ])
      } else {
        reject('Failed to fetch data!')
      }
    }, 1000)
  })
}

Para o resultado bem-sucedido, você retorna objetos JavaScript que representam dados de usuário de amostra.

Para lidar com o erro, você usará o método de instância catch. Isso fornecerá um retorno de chamada de falha com o error como parâmetro.

Execute o comando getUser com onSuccess definido como false, usando o método then para o caso de sucesso e o método catch para o erro:

// Execute a função getUsers com a bandeira false para desencadear um erro
getUsers(false)
  .then((response) => {
    console.log(response)
  })
  .catch((error) => {
    console.error(error)
  })

Desde que o erro foi acionado, o then será ignorado e o catch lidará com o erro:

Output
Failed to fetch data!

Se você alternar a sinalização e resolve em vez disso, o catch será ignorado e os dados serão retornados:

// Execute a função getUsers com a flag true para resolver com sucesso
getUsers(true)
  .then((response) => {
    console.log(response)
  })
  .catch((error) => {
    console.error(error)
  })

Isto resultará nos dados do usuário:

Output
(3) [{…}, {…}, {…}] 0: {id: 1, name: "Jerry"} 1: {id: 2, name: "Elaine"} 3: {id: 3, name: "George"}

Para referência, aqui está uma tabela com os métodos de manipulação em objetos Promise:

Method Description
then() Handles a resolve. Returns a promise, and calls onFulfilled function asynchronously
catch() Handles a reject. Returns a promise, and calls onRejected function asynchronously
finally() Called when a promise is settled. Returns a promise, and calls onFinally function asynchronously

As Promises podem ser confusas, tanto para novos desenvolvedores quanto para programadores experientes que nunca trabalharam em um ambiente assíncrono antes. No entanto, como mencionado, é muito mais comum consumir promises do que criá-las. Normalmente, uma API da Web do navegador ou uma biblioteca de terceiros fornecerá a promise, e você só precisa consumi-la.

Na seção final de promessas, este tutorial citará um caso de uso comum de uma API da Web que retorna promessas: a Fetch API.

Usando a Fetch API com Promessas

Uma das APIs da Web mais úteis e frequentemente utilizadas que retorna uma promessa é a Fetch API, que permite fazer uma solicitação de recurso assíncrono pela rede. fetch é um processo de duas partes e, portanto, requer encadeamento de then. Este exemplo demonstra acessar a API do GitHub para buscar os dados de um usuário, enquanto também trata qualquer erro potencial:

// Buscar um usuário na API do GitHub
fetch('https://api.github.com/users/octocat')
  .then((response) => {
    return response.json()
  })
  .then((data) => {
    console.log(data)
  })
  .catch((error) => {
    console.error(error)
  })

A solicitação fetch é enviada para o URL https://api.github.com/users/octocat, que espera assincronamente por uma resposta. O primeiro then passa a resposta para uma função anônima que formata a resposta como dados JSON, em seguida, passa o JSON para um segundo then que registra os dados no console. O comando catch registra qualquer erro no console.

A execução deste código produzirá o seguinte:

Output
login: "octocat", id: 583231, avatar_url: "https://avatars3.githubusercontent.com/u/583231?v=4" blog: "https://github.blog" company: "@github" followers: 3203 ...

Estes são os dados solicitados de https://api.github.com/users/octocat, renderizados em formato JSON.

Esta seção do tutorial mostrou que promessas incorporam muitas melhorias para lidar com código assíncrono. No entanto, enquanto usar then para lidar com ações assíncronas é mais fácil de seguir do que a pirâmide de callbacks, alguns desenvolvedores ainda preferem um formato síncrono para escrever código assíncrono. Para atender a essa necessidade, ECMAScript 2016 (ES7) introduziu funções async e a palavra-chave await para facilitar o trabalho com promessas.

Funções Assíncronas com async/await

Uma função assíncrona permite que você lide com código assíncrono de uma maneira que parece síncrona. As funções async ainda utilizam promessas por baixo dos panos, mas possuem uma sintaxe mais tradicional em JavaScript. Nesta seção, você irá experimentar exemplos dessa sintaxe.

Você pode criar uma função async adicionando a palavra-chave async antes de uma função:

// Criar uma função async
async function getUser() {
  return {}
}

Embora esta função ainda não esteja lidando com nada assíncrono, ela se comporta de maneira diferente de uma função tradicional. Se você executar a função, perceberá que ela retorna uma promessa com um [[PromiseStatus]] e [[PromiseValue]] em vez de um valor de retorno.

Tente isso registrando uma chamada à função getUser:

console.log(getUser())

Isto dará o seguinte:

Output
__proto__: Promise [[PromiseStatus]]: "fulfilled" [[PromiseValue]]: Object

Isso significa que você pode lidar com uma função async com then da mesma maneira que poderia lidar com uma promessa. Experimente isso com o seguinte código:

getUser().then((response) => console.log(response))

Esta chamada para getUser passa o valor de retorno para uma função anônima que registra o valor no console.

Você receberá o seguinte quando executar este programa:

Output
{}

Uma função async pode manipular uma promise chamada dentro dela usando o operador await. O await pode ser usado dentro de uma função async e aguardará até que uma promise seja resolvida antes de executar o código designado.

Com esse conhecimento, você pode reescrever a solicitação Fetch da última seção usando async/await da seguinte maneira:

// Manipular fetch com async/await
async function getUser() {
  const response = await fetch('https://api.github.com/users/octocat')
  const data = await response.json()

  console.log(data)
}

// Executar função async
getUser()

Os operadores await aqui garantem que os data não sejam registrados antes que a solicitação os tenha preenchido com dados.

Agora o data final pode ser tratado dentro da função getUser, sem necessidade de usar then. Este é o resultado do registro do data:

Output
login: "octocat", id: 583231, avatar_url: "https://avatars3.githubusercontent.com/u/583231?v=4" blog: "https://github.blog" company: "@github" followers: 3203 ...

Observação: Em muitos ambientes, async é necessário para usar await — no entanto, algumas novas versões de navegadores e o Node permitem o uso de await no nível superior, o que permite evitar a criação de uma função async para envolver o await.

Finalmente, como você está lidando com a promise cumprida dentro da função assíncrona, você também pode lidar com o erro dentro da função. Em vez de usar o método catch com then, você usará o padrão try/catch para lidar com a exceção.

Adicione o seguinte código destacado:

// Lidar com sucesso e erros com async/await
async function getUser() {
  try {
    // Lidar com sucesso em try
    const response = await fetch('https://api.github.com/users/octocat')
    const data = await response.json()

    console.log(data)
  } catch (error) {
    // Lidar com erro em catch
    console.error(error)
  }
}

O programa agora irá pular para o bloco catch se receber um erro e registrar esse erro no console.

O código assíncrono moderno em JavaScript é mais frequentemente tratado com a sintaxe de async/await, mas é importante ter um conhecimento prático de como as promessas funcionam, especialmente porque as promessas são capazes de recursos adicionais que não podem ser tratados com async/await, como combinar promessas com Promise.all().

Nota: async/await pode ser reproduzido usando geradores combinados com promessas para adicionar mais flexibilidade ao seu código. Para saber mais, confira nosso tutorial Entendendo Geradores em JavaScript.

Conclusão

Porque as APIs da Web frequentemente fornecem dados de forma assíncrona, aprender a lidar com o resultado de ações assíncronas é uma parte essencial de ser um desenvolvedor JavaScript. Neste artigo, você aprendeu como o ambiente hospedeiro utiliza o loop de eventos para lidar com a ordem de execução de código com a pilha e fila. Você também experimentou exemplos de três maneiras de lidar com o sucesso ou falha de um evento assíncrono, com callbacks, promessas e sintaxe async/await. Por fim, você utilizou a API Fetch da Web para lidar com ações assíncronas.

Para obter mais informações sobre como o navegador lida com eventos paralelos, leia Modelo de Concorrência e o Loop de Eventos na Rede de Desenvolvedores Mozilla. Se você gostaria de aprender mais sobre JavaScript, retorne à nossa série Como Programar em JavaScript.

Source:
https://www.digitalocean.com/community/tutorials/understanding-the-event-loop-callbacks-promises-and-async-await-in-javascript