Skip to content
This page has been auto-translated and may contain errors.View in English

JavaScript Assíncrono

docs.scrimba.com

Algumas coisas acontecem instantaneamente: somar dois números, ler uma variável, mudar texto na página. Outras levam tempo. Um temporizador conta regressivamente por três segundos. Uma solicitação para um servidor viaja pela rede e volta um momento depois. Se o JavaScript parasse e esperasse por cada uma delas, a página inteira congelaria: sem cliques, sem rolagem, nada, até que a coisa lenta terminasse. Não funciona assim. Este capítulo é sobre como o JavaScript inicia algo lento, mantém a página responsiva e volta para lidar com o resultado quando ele está pronto.

Por que assíncrono

A maioria do JavaScript é executada de cima para baixo, uma linha terminando antes da próxima iniciar. Isso é bom para trabalho rápido. Mas algumas tarefas levam tempo real, e o JavaScript não fica esperando por elas. Ele inicia a tarefa lenta, passa para a próxima linha e volta para a lenta quando termina. Isso é o que assíncrono significa: não acontecendo em ordem, não agora, mas depois, quando o resultado está pronto.

O exemplo mais simples é um temporizador. setTimeout executa um pedaço de código após um atraso:

js
console.log("Início");
setTimeout(() => {
  console.log("3 segundos depois");
}, 3000);
console.log("Fim");
// Início
// Fim
// 3 segundos depois

Veja a ordem. "Fim" é exibido antes de "3 segundos depois", mesmo que venha depois de setTimeout no código. O JavaScript não esperou três segundos nessa linha. Ele configurou o temporizador, continuou, e executou o código atrasado quando o tempo acabou.

O JavaScript executa uma coisa por vez, de cima para baixo. Esse modelo funciona até que uma tarefa leve tempo: um temporizador, uma solicitação de rede, leitura de um arquivo. Se a linguagem bloqueasse naquelas, a página congelaria enquanto esperava, porque nada mais poderia ser executado. Código assíncrono é a resposta: você inicia a tarefa lenta, o JavaScript continua executando o resto do seu código, e o resultado é tratado depois quando chega.

setTimeout é o exemplo primeiro mais claro. Você passa uma função e um atraso em milissegundos, e ela executa essa função após o atraso:

js
console.log("Início");
setTimeout(() => {
  console.log("3 segundos depois");
}, 3000);
console.log("Fim");
// Início
// Fim
// 3 segundos depois

A função que você passa para setTimeout é um callback: código que você entrega para ser chamado depois, não agora. O JavaScript registra o temporizador, vai direto para "Fim", e só executa o callback uma vez que o atraso passou e o código atual terminou. Esse padrão "termine agora, volte depois" é toda a ideia do assíncrono, e tudo mais neste capítulo se constrói sobre isso.

JavaScript é single-threaded: uma call stack, uma coisa executando de cada vez. Essa restrição é exatamente por que o assíncrono importa. Uma espera bloqueante em uma solicitação de rede travaria a thread única, e com ela cada clique, animação e renderização na página. Então trabalho lento não é feito inline. É entregue ao runtime (o navegador ou Node), que faz a espera fora da sua thread e agenda seu código para retomar quando o resultado está pronto. Código assíncrono é aquela entrega: você descreve o que fazer com um resultado que você não tem ainda.

setTimeout é o caso mínimo, e expõe o modelo de forma clara:

js
console.log("Início");
setTimeout(() => {
  console.log("depois");
}, 0);
console.log("Fim");
// Início
// Fim
// depois

O atraso aqui é 0, e o callback ainda é executado por último. Esse é o indicador: callbacks assíncrono nunca são executados no meio do código síncrono. setTimeout não executa seu callback após zero milissegundos, ele agenda o callback para ser executado após o código síncrono atual drenar. O mecanismo por trás daquela ordenação é o event loop, que a última seção deste capítulo desmonta. Por enquanto, mantenha a regra: código síncrono é executado até completar, depois trabalho assíncrono na fila.

JunoPor que assíncrono Algumas tarefas levam tempo, como um temporizador ou carregamento de dados, e o JavaScript não congela enquanto espera. Ele inicia a coisa lenta, continua executando as próximas linhas, e volta para a coisa lenta quando está pronta. É por isso que "Fim" é exibido antes da mensagem de setTimeout, mesmo que o temporizador venha primeiro no código.
JunoPor que assíncrono Assíncrono significa iniciar a tarefa lenta agora, lidar com seu resultado depois, e manter a página responsiva enquanto isso. setTimeout é a versão mais simples: você passa um callback para ele e ele executa esse callback após o atraso, não na hora. Uma vez que você vê por que a mensagem atrasada é exibida por último, o resto deste capítulo são variações naquela ideia.
JunoPor que assíncrono JavaScript é single-threaded, então bloquear em trabalho lento travaria tudo. Em vez disso você entrega a espera ao runtime e descreve o que fazer com o resultado quando chega. O caso setTimeout(fn, 0) é o que denota: o callback ainda é executado após o código síncrono, porque trabalho assíncrono na fila espera o código atual terminar primeiro.

Promises

setTimeout é bom para um atraso, mas a maioria do trabalho assíncrono produz um valor que você se importa: os dados que você pediu, ou um erro se algo deu errado. Para isso, JavaScript usa uma promise. Uma promise é um valor que ainda não está pronto, com um espaço reservado para ele. É como um recibo: você não tem a refeição, mas tem algo que se transformará na refeição.

Uma promise termina de uma de duas maneiras. Ela resolve com um valor quando o trabalho é bem-sucedido, ou rejeita com um erro quando falha. Você diz o que fazer em cada caso com .then() e .catch():

js
loadUser()
  .then((user) => {
    console.log(`Carregado ${user.name}`);
  })
  .catch((error) => {
    console.log("Algo deu errado");
  });

.then() é executado quando a promise resolve, e recebe o valor. .catch() é executado se rejeita, e recebe o erro. O código dentro deles é executado depois, uma vez que o resultado chega.

Callbacks funcionam, mas se empilham mal. Quando uma tarefa assíncrona depende de outra, e aquela de outra, você acaba aninhando callbacks dentro de callbacks, se movendo mais para a direita a cada passo. Aquela forma tem um nome, a "pirâmide da perdição", e fica difícil de ler e mais difícil de lidar com erros. Uma promise corrige a forma.

Uma promise é um objeto representando um valor que ainda não está pronto. Está em um de três estados: pendente (ainda funcionando), cumprida (resolvida com um valor), ou rejeitada (falhou com um erro). Você anexa handlers com .then() para sucesso e .catch() para falha:

js
loadUser()
  .then((user) => {
    console.log(`Carregado ${user.name}`);
    return loadPosts(user.id);
  })
  .then((posts) => {
    console.log(`Encontrados ${posts.length} posts`);
  })
  .catch((error) => {
    console.log(`Falha: ${error.message}`);
  });

Duas coisas tornam isso melhor que callbacks aninhados. Primeiro, é plano: cada .then() retorna uma promise, então você encadeia elas em uma linha reta em vez de aninhar. Retorne um valor de um .then() e o próximo recebe; retorne uma promise e a corrente espera. Segundo, um .catch() no final trata uma falha em qualquer lugar da corrente, então você escreve tratamento de erro uma vez em vez de em cada nível.

Antes de promises, resultados assíncrono eram entregues através de callbacks, e etapas assíncrono dependentes significavam aninhar callbacks dentro de callbacks. Além do custo de legibilidade, aquele padrão não tem caminho de erro unificado: cada nível trata sua própria falha, e esquecer uma engole o erro silenciosamente. Uma promise resolve ambos os problemas tornando o resultado eventual um valor de primeira classe que você pode passar por aí, encadear, e tratar em um lugar.

Uma promise é um objeto em um de três estados: pendente, cumprida com um valor, ou rejeitada com uma razão. Uma vez que ela se estabelece (cumprida ou rejeitada) nunca muda novamente. Você registra reações com .then(onFulfilled) e .catch(onRejected), e o encadeamento é onde o poder real está:

js
loadUser()
  .then((user) => loadPosts(user.id)) // retorna uma promise, corrente espera por ela
  .then((posts) => posts.filter((post) => post.published))
  .then((published) => console.log(`${published.length} publicados`))
  .catch((error) => console.log(`Falha: ${error.message}`));

Cada .then() retorna uma nova promise, e o valor de retorno do handler a determina: retorne um valor comum e o próximo .then() recebe aquele valor, retorne uma promise e a corrente a adota e espera. Um erro lançado ou uma rejeição em qualquer lugar pula os handlers .then() restantes e salta para o próximo .catch(), que é por isso um handler no final pega falhas de qualquer etapa. Isso é uma real melhora sobre callbacks, não apenas uma sintaxe mais legal: erros se propagam automaticamente, e a corrente plana lê na ordem que é executada. Na prática você lerá muitas cadeias de promises, mas escreverá a maioria de seu próprio código assíncrono com async/await, que é a próxima seção e se senta exatamente no topo daquela maquinaria.

JunoPromises Uma promise é um valor que não está pronto ainda, como um recibo para um resultado que chega depois. Ela resolve com um valor quando o trabalho é bem-sucedido, ou rejeita com um erro quando falha. Você diz o que fazer com .then() para o valor e .catch() para o erro, e ambos são executados depois uma vez que o resultado está lá.
JunoPromises Uma promise representa um resultado que ainda está chegando, e bate callbacks aninhados porque você encadeia .then() em uma linha plana em vez de se mover para a direita. Retorne um valor de um .then() e o próximo o consegue, retorne uma promise e a corrente espera. Um .catch() no final trata uma falha em qualquer lugar da corrente, então você escreve tratamento de erro uma vez.
JunoPromises Uma promise é um objeto que se estabelece uma vez, para cumprida ou rejeitada, e nunca muda depois. Cada .then() retorna uma nova promise, então retornar um valor a passa adiante e retornar uma promise faz a corrente esperar, enquanto qualquer rejeição pula adiante para o próximo .catch(). Você lerá muitas correntes, mas escreverá a maioria do seu assíncrono com async e await, que se sentam bem no topo disso.

async / await

Cadeias de promises funcionam, mas há uma forma mais limpa de escrevê-las que lê quase como código comum de cima para baixo. Usa duas palavras-chave, async e await.

Você marca uma função async, e dentro dela você pode await uma promise. await pausa a função até que a promise resolve, depois lhe dá o valor diretamente, sem .then() necessário:

js
async function showUser() {
  const user = await loadUser();
  console.log(`Carregado ${user.name}`);
}

Leia aquilo de cima para baixo: obtenha o usuário, depois registre o nome. A linha await espera que loadUser() termine e lhe entrega o user. A função pausa ali, mas o resto da página continua rodando, então nada congela.

Para tratar erros, envolva o await em try e catch:

js
async function showUser() {
  try {
    const user = await loadUser();
    console.log(`Carregado ${user.name}`);
  } catch (error) {
    console.log("Não foi possível carregar o usuário");
  }
}

Se a promise falhar, o código salta para o bloco catch em vez de travar.

async/await é a forma moderna de escrever código assíncrono. É construído sobre promises, então nada muda por baixo, mas permite que você escreva lógica assíncrona que lê como o código síncrono que você já conhece de funções.

Marque uma função async e você pode usar await dentro dela. await pega uma promise, pausa a função até que resolve, e avalia para o valor resolvido:

js
async function showUser() {
  const user = await loadUser();
  const posts = await loadPosts(user.id);
  console.log(`${user.name} tem ${posts.length} posts`);
}

Compare aquilo com a corrente .then() da última seção: mesmo trabalho, mas plano e linear, com os valores descendo em bindings const comuns que você pode usar na próxima linha. Uma função async sempre retorna uma promise ela mesma, então chamar showUser() lhe dá uma promise que você pode await ou .then() de outro lugar.

Erros são a parte que as pessoas perdem. Uma promise rejeitada dentro de uma função async lança, então você a pega com um try/catch comum:

js
async function showUser() {
  try {
    const user = await loadUser();
    console.log(`Carregado ${user.name}`);
  } catch (error) {
    console.log(`Falha: ${error.message}`);
  }
}

O mesmo try/catch que você usaria para um erro síncrono trata um assíncrono, que é uma grande parte do por que async/await venceu.

async/await é sintaxe sobre promises. Uma função async sempre retorna uma promise, e await desembrulha uma: ela suspende a função, cede a thread de volta ao runtime, e retoma com o valor resolvido quando a promise se estabelece. Nada é bloqueado enquanto espera. A função é pausada, mas a thread única é livre para executar outro trabalho, que é todo o ponto.

O benefício é que código assíncrono recupera as duas coisas que callbacks e cadeias brutas lhe custam: ordem de leitura linear e controle de fluxo normal. await permite que você use const, condicionais, e loops ao redor de valores assíncrono como se fossem síncronos:

js
async function publishReport(userId) {
  const user = await loadUser(userId);
  if (!user.active) {
    return null; // retorno antecipado funciona normalmente
  }
  const posts = await loadPosts(user.id);
  return posts.filter((post) => post.published);
}

Tratamento de erro se colapsa em try/catch porque uma promise rejeitada aguardada lança no await:

js
async function publishReport(userId) {
  try {
    const user = await loadUser(userId);
    return await loadPosts(user.id);
  } catch (error) {
    console.log(`Relatório falhou: ${error.message}`);
    return [];
  }
}

Uma sutileza vale a pena internalizar: return await dentro de um try importa. Se você escrever return loadPosts(...) sem await, a função retorna a promise e sai do try antes de a promise se estabelecer, então uma rejeição escapa do catch. Com await, a rejeição é lançada enquanto você ainda está dentro do try, e o catch a vê. Esse é o tipo de detalhe que se torna um bug "por que meu handler de erro não disparou", e a próxima seção sobre o event loop e padrões de erro vai mais fundo nisso.

Junoasync e await Marque uma função async e você pode await uma promise dentro dela. await pausa aquela função até que a promise resolve e lhe entrega o valor direto, então o código lê de cima para baixo sem .then(). Envolva em try e catch para tratar falhas, e a página continua rodando enquanto a função espera.
Junoasync e awaitasync/await é promises com sintaxe mais limpa: await pausa a função até que uma promise resolve e lhe dá o valor em um const comum. Uma função async sempre retorna uma promise, então você pode await dela de outro lugar. Erros lançam, então um try/catch comum os trata, e essa é a maioria de por que esse estilo venceu.
Junoasync e awaitawait suspende a função e cede a thread, depois retoma com o valor resolvido, então nada bloqueia enquanto você espera. Você consegue leitura linear, controle de fluxo normal, e try/catch para erros. Veja return await dentro de um try: solte o await e uma rejeição escapa antes de o catch vê-la.

Busca de dados

A tarefa assíncrona mais comum que você escreverá é carregar dados de um servidor. O navegador lhe dá fetch para isso. Você passa uma URL para ela, e ela retorna uma promise para a resposta:

js
async function loadUsers() {
  const response = await fetch("https://api.example.com/users");
  const users = await response.json();
  console.log(users);
}

Há dois awaits, e isso surpreende as pessoas no início. O primeiro espera que o servidor responda. O segundo lê o corpo daquela resposta e o transforma em dados JavaScript com .json(), que é em si assíncrono. Então buscar é dois passos: obter a resposta, depois lê-la.

Servidores também podem responder com um erro, como "não encontrado". fetch não trata aquilo como uma falha por si só, então você verifica response.ok você mesmo:

js
async function loadUsers() {
  const response = await fetch("https://api.example.com/users");
  if (!response.ok) {
    console.log("Solicitação falhou");
    return;
  }
  const users = await response.json();
  console.log(users);
}

Carregar dados de um servidor é a tarefa assíncrona que você alcançará mais, e o fetch do navegador é como você faz. Dê uma URL para ele e retorna uma promise que resolve para um objeto Response:

js
async function loadUsers() {
  const response = await fetch("https://api.example.com/users");
  if (!response.ok) {
    throw new Error(`Solicitação falhou: ${response.status}`);
  }
  const users = await response.json();
  return users;
}

A natureza de dois passos é a parte para manter. O primeiro await resolve quando os headers da resposta chegam, dando a você uma Response. Aquela resposta não é os dados ainda, é uma handle para isso. Ler o corpo é um segundo passo assíncrono: response.json() retorna uma promise que resolve para os dados analisados. Dois awaits, dois estágios.

A pegadinha que pega todos: fetch só rejeita em uma falha de rede, não em um status de erro HTTP. Um 404 ou 500 ainda resolve; verifique response.ok você mesmo. response.ok é true para códigos de status nos 200s, então uma guarda em !response.ok é como você transforma um status ruim em um erro real. Uma vez que você lança lá, um try/catch ao redor da chamada trata ambos os erros de rede e respostas ruins em um único lugar.

fetch(url) é o cliente HTTP assíncrono do navegador, e retorna uma promise que resolve para uma Response. O design tem uma aresta afiada que vale a pena afirmar antecipadamente: a promise só rejeita em uma falha de nível de rede, uma conexão perdida ou uma solicitação bloqueada. Um status de erro HTTP como 404 ou 500 é uma viagem redonda bem-sucedida como longe como fetch se preocupa, então resolve normalmente. Verificar response.ok não é opcional.

js
async function loadUsers() {
  const response = await fetch("https://api.example.com/users");
  if (!response.ok) {
    throw new Error(`Solicitação falhou: ${response.status}`);
  }
  return await response.json();
}

A forma de dois estágios reflete streaming. O primeiro await se estabelece quando os headers da resposta chegam, antes do corpo ter necessariamente chegado. A Response é uma handle para um stream, e response.json() lê aquele stream até o final e o analisa, que é por isso que é ela mesma assíncrona e precisa de seu próprio await. Transformar um status ruim em um erro lançado dentro da função significa um try/catch no site de chamada cobre falha de rede, status ruim, e JSON que falha em analisar, três modos de falha diferentes roteados para um handler:

js
try {
  const users = await loadUsers();
  render(users);
} catch (error) {
  console.log(`Não foi possível carregar usuários: ${error.message}`);
}

Essa é toda a anatomia prática de uma busca: uma promise para a resposta, uma verificação de status, uma segunda promise para o corpo, e uma único limite de erro ao redor de tudo. A próxima seção cobre o que acontece quando você precisa de vários destes ao mesmo tempo.

JunoBuscando dadosfetch(url) retorna uma promise para a resposta do servidor, e carregar dados é dois passos: await fetch(...) para a resposta, depois await response.json() para ler os dados de dentro dela. Um erro de servidor como "não encontrado" não falha por si só, então verifique response.ok antes de ler o corpo. Ambos os passos usam await porque ambos levam tempo.
JunoBuscando dadosfetch retorna uma promise para uma Response, e ler o corpo com response.json() é um segundo passo assíncrono, então uma busca normal tem dois awaits. A armadilha: fetch só rejeita em uma falha de rede, então um 404 ou 500 ainda resolve. Verifique response.ok e lance em um status ruim, e um try/catch cobre tudo.
JunoBuscando dadosfetch resolve quando os headers chegam, não o corpo, que é por isso response.json() é um passo separado aguardado que lê o stream. Ele só rejeita em uma falha de rede, então um 404 resolve bem e você deve verificar response.ok você mesmo. Lance em um status ruim e um try/catch no site de chamada trata erros de rede, status ruins, e falhas de análise juntos.

O event loop e executando trabalho em paralelo

Tudo até agora tem um tema: JavaScript inicia trabalho lento, continua, e volta para isso depois. Esta seção é o mecanismo por trás disso, mais os padrões para executar várias tarefas assíncrono bem.

Aqui está uma figura de como o JavaScript mantém o controle de trabalho assíncrono. Seu código em execução fica na call stack, a lista do que está sendo executado agora. Quando você chama setTimeout, o temporizador é entregue ao navegador para contar regressivamente, então não está na stack bloqueando nada. Quando o temporizador termina, seu callback vai para uma fila de tarefas, uma fila de espera de código pronto para ser executado.

O event loop é a parte que os conecta. Tem uma regra: execute tudo na call stack primeiro, e só quando a stack estiver vazia puxe a próxima tarefa da fila. É por isso que setTimeout(fn, 0) ainda é executado após seu outro código, mesmo com um atraso zero. O callback tem que esperar na fila até que o código atual termine:

js
console.log("primeiro");
setTimeout(() => console.log("terceiro"), 0);
console.log("segundo");
// primeiro
// segundo
// terceiro

O atraso de 0 não significa "agora". Significa "assim que o código atual terminar".

O event loop é o mecanismo que faz toda essa ordenação funcionar. Três partes: a call stack (o código síncrono rodando agora), a fila de tarefas (callbacks assíncrono esperando sua vez), e o loop em si, que faz uma coisa: quando a call stack está vazia, pega a próxima tarefa da fila e a executa.

Aquela regra única explica a ordenação de setTimeout(fn, 0). O atraso é uma espera mínima antes de o callback ser enfileirado, não uma promessa de executar imediatamente, e uma tarefa enfileirada não pode ser executada até que o código síncrono atual termine e limpe a stack:

js
console.log("primeiro");
setTimeout(() => console.log("terceiro"), 0);
console.log("segundo");
// primeiro
// segundo
// terceiro

Quando você tem várias tarefas assíncrono independentes, executá-las uma após a outra desperdiça tempo. Aguardar em um loop é o erro comum:

js
// Lento: cada solicitação espera a anterior terminar
const users = [];
for (const id of ids) {
  users.push(await loadUser(id));
}

Se as tarefas não dependem uma da outra, inicie-as juntas e espere por todas de uma vez com Promise.all. Ela pega um array de promises e retorna uma promise que resolve para um array de seus resultados:

js
// Rápido: todas as solicitações rodam ao mesmo tempo
const users = await Promise.all(ids.map((id) => loadUser(id)));

Alcance Promise.all sempre que você tiver trabalho assíncrono independente, e mantenha o await sequencial em um loop só quando cada passo realmente precisa do resultado do anterior.

O event loop é o agendador que torna o assíncrono single-threaded possível. A call stack mantém os frames síncronos sendo executados agora. Callbacks assíncrono não empurram para ela diretamente; eles esperam em uma fila. A regra do loop é simples: quando a call stack está vazia, desenfileira a próxima tarefa e a executa até completar, depois repita. Uma tarefa é executada sem interrupção, que é por isso trabalho síncrono longo ainda bloqueia tudo, assíncrono ou não.

Aquele modelo é a explicação completa de setTimeout(fn, 0). O atraso agenda o callback na fila após um tempo mínimo, mas não pode ser executado até que a stack drene, então sempre segue o código síncrono:

js
console.log("primeiro");
setTimeout(() => console.log("terceiro"), 0);
console.log("segundo");
// primeiro, segundo, terceiro

Um refinamento para manter na sua cabeça: callbacks de promise (os handlers .then e tudo após um await) vão para uma fila de microtarefa separada, de prioridade mais alta, que drena completamente entre tarefas, então a continuação de uma promise resolvida é executada antes de um setTimeout enfileirado. Você raramente precisa raciocinar sobre isso, mas explica o ocasional auspício onde código de promise bate um temporizador.

O benefício prático é agendar seu próprio trabalho bem. Tarefas assíncrono independentes devem ser executadas em paralelo, não em série. Aguardar dentro de um loop as serializa:

js
// Série: tempo total é a soma de cada solicitação
for (const id of ids) {
  results.push(await loadUser(id));
}

// Paralelo: tempo total é aproximadamente a solicitação única mais lenta
const results = await Promise.all(ids.map((id) => loadUser(id)));

Promise.all inicia cada promise imediatamente (o .map as inicia todas), depois resolve uma vez que todas se estabelecem, rejeitando assim que qualquer uma rejeita. Use para trabalho independente; mantenha await sequencial só quando uma etapa precisa do resultado anterior. Uma cautela em condições de corrida: se duas operações assíncrono escrevem para o mesmo estado e você não controla sua ordem, a última a terminar vence, que pode não ser a última que você iniciou. Quando ordem importa, await a primeira antes de iniciar a segunda, ou chaveie cada resultado para que uma chegada tardia não possa sobrescrever uma mais nova.

JunoO event loop Seu código em execução fica na call stack, e quando um temporizador termina seu callback espera na fila de tarefas. O event loop executa tudo na stack primeiro, depois puxa a próxima tarefa esperando, que é por isso setTimeout(fn, 0) ainda é executado após o resto do seu código. Um atraso de 0 significa "assim que o código atual terminar", não "agora".
JunoO event loop O event loop executa a call stack até esvaziar, depois pega a próxima tarefa da fila, que é exatamente por que setTimeout(fn, 0) espera seu código síncrono terminar. Quando tarefas assíncrono não dependem uma da outra, não as await uma por uma em um loop. Inicie-as juntas e use Promise.all, e mantenha await sequencial só quando uma etapa precisa do resultado da última.
JunoO event loop O loop executa uma tarefa até completar, depois a próxima, então trabalho síncrono longo bloqueia tudo e setTimeout(fn, 0) sempre vem atrás do código atual. Continuações de promise drenam em uma fila de microtarefa de prioridade mais alta entre tarefas, que explica o tempo ímpar um .then bate um temporizador. Execute trabalho independente com Promise.all em vez de aguardar em um loop, e quando duas operações tocam no mesmo estado, controle a ordem ou uma conclusão tardia sobrescreve um resultado mais novo.

Para onde o assíncrono vai daqui

Assíncrono é a parte do JavaScript que mantém uma página viva enquanto trabalho lento acontece no fundo. A forma é sempre a mesma: inicie algo que leva tempo, deixe o resto do seu código ser executado, e trate o resultado quando chega. Promises dão aquele resultado um valor que você pode passar por aí, async/await permite que você o escreva em código simples de cima para baixo, e fetch é onde você o usará mais, carregando dados de um servidor.

Daqui em diante, assíncrono se conecta às partes da linguagem que reagem ao mundo exterior. Você ligará dados buscados à página através do DOM, e você iniciará trabalho assíncrono em resposta ao que as pessoas fazem na página, que é events. Os resultados que você consegue de volta são quase sempre objetos, então ler e remodelar eles é a próxima habilidade que se emparelha com tudo aqui.