Entendendo o Event Loop, Callbacks, Promises e Async/Await em 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, tem se tornado cada vez mais necessário realizar operações intensivas como fazer solicitações de rede externa para recuperar dados de APIs. 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 single-threaded com um modelo de execução síncrono que processa uma operação após a outra, ele só pode processar uma instrução por 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 maneira 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 o comportamento de bloqueio, o ambiente do navegador possui muitas APIs da Web às quais o JavaScript pode acessar de forma assíncrona, o que significa que 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 APIs da Web assíncronas e lidar com a resposta ou erro dessas operações. Neste artigo, você aprenderá sobre o loop de eventos, a forma original de lidar com o comportamento assíncrono por meio de callbacks, a adição atualizada de promessas do 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 verdadeiros no ambiente Node.js, no entanto, o Node.js usa suas próprias APIs em C++ em vez das APIs da Web do navegador. Para obter mais informações sobre programação assíncrona no Node.js, confira Como Escrever Código Assíncrono no Node.js.

O Loop de Eventos

Esta seção explicará como o JavaScript lida com código assíncrono usando o event loop. Primeiro, ele passará por uma demonstração do event loop em ação e, em seguida, explicará os dois elementos do event loop: a pilha e a fila.

O código JavaScript que não usa nenhuma API da Web assíncrona será executado de forma síncrona — um de cada vez, sequencialmente. Isso é demonstrado pelo código de exemplo a seguir, que chama três funções, cada uma imprimindo 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, faça 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 — first(), second(), depois third():

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 é setTimeout, que define um temporizador e executa uma ação após um período de tempo especificado. setTimeout precisa ser assíncrono, caso contrário, o navegador inteiro ficaria congelado durante a espera, o que resultaria em uma experiência do usuário ruim.

Adicione setTimeout à função second 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 a quantidade de tempo que esperará antes de chamar essa função. Neste código, você encapsulou console.log em uma função anônima e a passou para setTimeout, em seguida, definiu a função para ser executada após 0 milissegundos.

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

// Executar 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 resulte nos números sendo impressos em ordem sequencial. Mas porque é assíncrono, a função com o tempo de espera será impressa por último:

Output
1 3 2

Independentemente de você definir o tempo de espera 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 ocorre porque o ambiente hospedeiro 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 de cada vez, ele precisa que o loop de eventos seja informado de quando executar qual instrução específica. O loop de eventos lida com 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:

  • Adicionar primeiro() à pilha, executar primeiro() que registra 1 no console, remover primeiro() da pilha.
  • Adicionar segundo() à pilha, executar segundo() que registra 2 no console, remover segundo() da pilha.
  • Adicionar terceiro() à pilha, executar terceiro() que registra 3 no console, remover terceiro() da pilha.

O segundo exemplo com setTimeout parece assim:

  • Adicionar primeiro() à pilha, executar primeiro() que registra 1 no console, remover primeiro() da pilha.
  • Adicionar segundo() à pilha, executar segundo().
    • Adicionar setTimeout() à pilha, executar o setTimeout() Web API que inicia um temporizador e adiciona a função anônima à fila, remover 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 Web assíncrona, introduz o conceito da fila, que este tutorial abordará a seguir.

Fila

A fila, também referida como 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 esperando, 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 no 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 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 estivesse sendo executada no momento, o que poderia ter efeitos indesejados e imprevisíveis.

Nota: Existe também outra fila chamada fila de trabalhos ou fila de microtarefas que lida com promessas. Microtarefas como promessas são tratadas com prioridade mais alta do que macrotarefas 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 no 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 foi executada após tudo no contexto de execução principal de nível superior. Mas se você quisesse garantir que uma das funções, como a função terceira, fosse executada após o tempo limite, então você teria que usar métodos de codificação assíncrona. O tempo limite aqui pode representar uma chamada de API assíncrona que contém dados. Você deseja 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; elas 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 esta 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 é chamada de retorno de chamada
  callback()
}

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

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

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

Output
Just a function

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

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 entram os callbacks. 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, e então 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), será impresso 2 e então 3. Ao passar uma função como callback, você atrasou com sucesso a execução da função até que a API Web assíncrona (setTimeout) seja concluída.

A principal conclusão aqui é que as funções de retorno de chamada não são assíncronas – setTimeout é a API 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 foi concluída e lida com o sucesso ou falha da tarefa.

Agora que você aprendeu como usar funções de retorno de chamada para lidar com tarefas assíncronas, a próxima seção explica os problemas de aninhar muitas funções de retorno de chamada e criar uma “pirâmide do caos.”

Funções de Retorno de Chamada Aninhadas e a Pirâmide do Caos

As funções de retorno de chamada são uma maneira eficaz de garantir a execução atrasada de uma função até que outra seja concluída e retorne com dados. No entanto, devido à natureza aninhada das funções de retorno de chamada, o código pode ficar 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 de JavaScript no início, e como resultado, o código contendo funções de retorno de chamada aninhadas é frequentemente chamado de “pirâmide do caos” ou “inferno das funções de retorno de chamada.”

Aqui está uma demonstração de funções de retorno de chamada aninhadas:

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

Neste código, cada novo setTimeout é aninhado dentro de uma função de ordem superior, criando uma forma de pirâmide de funções de retorno de chamada mais e mais profundas. 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. Provavelmente você 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:

// Exemplo de função assíncrona
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 que pareça que a função assíncrona forjada
      // 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 response e um possível error, tornando a função callbackHell visualmente confusa.

Executar este código lhe dará 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

Esta forma de lidar com código assíncrono é difícil de acompanhar. Como resultado, o conceito de promessas foi introduzido no ES6. Isso é 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 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 de pending e um valor undefined:

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

Até agora, nada foi configurado para a promessa, então ela vai permanecer em estado pending 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ê descobrirá que ela tem um status de fulfilled e um value definido com o valor que você passou para resolve:

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

Conforme declarado no início desta seção, uma promessa é um objeto que pode retornar um valor. Após ser cumprida com sucesso, o value passa de undefined para ser populado 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 falhou, 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 quer ser capaz de acessar o valor. As promessas têm um método chamado then que será executado após uma promessa atingir resolve no código. then retornará o valor da promessa como um parâmetro.

Assim é como 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 Conseguimos!. Este 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)
})

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

Usar a sintaxe then garante que a response será registrada apenas 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á cumprido com o valor retornado 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 irá registrar o valor retornado:

Output
Resolving an asynchronous request! And chaining!

Já que 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.

Manipulação de Erros

Até agora, você apenas lidou com uma promessa com um resolve bem-sucedido, o 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 de erro de criação e consumo de uma promessa.

Esta função getUsers passará uma sinalização 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 de modo que se onSuccess for true, o timeout será resolvido 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 exemplo.

Para lidar com o erro, você usará o método de instância catch. Isso lhe dará 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 sinalização falsa para acionar 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ê trocar a flag e resolve ao invés, 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 desenvolvedores novos quanto para programadores experientes que nunca trabalharam em um ambiente assíncrono antes. No entanto, como mencionado, é muito mais comum consumir promises do que criar. Normalmente, uma API Web do navegador ou uma biblioteca de terceiros fornecerá a promise, e você só precisa consumi-la.

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

Usando a Fetch API com Promises

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íncrona pela rede. fetch é um processo de duas partes e, portanto, requer encadeamento de then. Este exemplo demonstra o uso da API do GitHub para buscar os dados de um usuário, enquanto também lida com possíveis erros:

// 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 a URL https://api.github.com/users/octocat, que aguarda assincronamente 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. A instrução catch registra qualquer erro no console.

A execução deste código resultará no 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 no formato JSON.

Esta seção do tutorial mostrou que as 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 async permite que você lide com código assíncrono de uma maneira que parece síncrona. As funções async ainda usam promises por baixo dos panos, mas têm uma sintaxe mais tradicional do JavaScript. Nesta seção, você experimentará exemplos dessa sintaxe.

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

// Crie 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, verá que ela retorna uma promise com um [[PromiseStatus]] e [[PromiseValue]] em vez de um valor de retorno.

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

console.log(getUser())

Isso resultará no seguinte:

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

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

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

Essa 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 lidar com uma promessa chamada dentro dela usando o operador await. O await pode ser usado dentro de uma função async e esperará até que uma promessa seja resolvida antes de executar o código designado.

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

// Lidar com fetch usando 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 assíncrona
getUser()

Os operadores await aqui garantem que os dados não sejam registrados antes que a requisição os preencha com dados.

Agora os dados finais podem ser tratados dentro da função getUser, sem a necessidade de usar then. Este é o resultado do registro de dados:

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 Node permitem o uso de await no nível superior, o que permite ignorar a criação de uma função assíncrona para envolver o await.

Finalmente, como você está lidando com a promessa 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 async/await, mas é importante ter um conhecimento prático de como 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 Generators 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 usa o loop de eventos para lidar com a ordem de execução do código com a pilha e a 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 a sintaxe async/await. Por fim, você utilizou a Fetch Web API para lidar com ações assíncronas.

Para mais informações sobre como o navegador lida com eventos paralelos, leia Modelo de Concorrência e o Loop de Eventos na Mozilla Developer Network. Se você deseja 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