Pular para o conteúdo principal

Extra A: Assincronismo em TypeScript

Esta página não é uma aula da grade. É material complementar, de linguagem, que você pode ler em qualquer ponto do semestre — e que provavelmente faz mais sentido depois de ter escrito algum código de verdade com a stack, quando as perguntas já apareceram.

O assincronismo aparece em vários pontos da disciplina: nas consultas ao Prisma, nos serviços do NestJS que acessam o banco e nos componentes de servidor do Next.js que aguardam dados. Nem todo serviço ou componente precisa ser assíncrono: a Aula 6, por exemplo, ainda usa um acervo em memória e métodos síncronos. Esta página explica o mecanismo por trás de async e await.

Pré-requisito de leitura: funções, arrays, módulos e tipos básicos da Aula 3. O domínio continua sendo a biblioteca; os empréstimos deste laboratório são uma simplificação por livro, sem reproduzir o modelo de exemplares e leitores das Aulas 8 e 9.

Material extra e opcional

Nada aqui é pré-requisito de nenhuma aula, e nenhuma aula depende desta página. Ela existe para o momento em que o await deixa de ser ritual e passa a ser decisão — quando você precisa saber por que uma listagem demora três vezes mais do que deveria, ou por que um try não capturou o erro que estava bem ali.

Objetivos​

Ao final deste material, você deve ser capaz de:

  • Prever a ordem de saída de um trecho que mistura código síncrono, microtasks e macrotasks, justificando a previsão pelo modelo de execução.
  • Explicar o que uma Promise representa, quais são seus três estados e por que a transição entre eles acontece uma única vez.
  • Converter código em estilo de callback para async/await, e descrever o que a conversão elimina além do aninhamento.
  • Decidir entre execução sequencial e concorrente considerando dependências e limites de recursos, e implementar a decisão com Promise.all.
  • Distinguir concorrência, paralelismo e assincronismo, e classificar uma carga como limitada por CPU ou por entrada e saída para escolher entre Promise.all e uma thread adicional.
  • Diagnosticar uma condição de corrida entre dois await em código de uma thread só e corrigi-la sem recorrer a travas.
  • Escolher entre all, allSettled, race e any diante de um requisito, e justificar a escolha pelo desfecho que cada um produz.
  • Impor tempo limite a uma operação e distinguir desistir de esperar de cancelar o trabalho.
  • Tipar funções assíncronas, o parâmetro de um catch e dados vindos de fora, sem recorrer a any.
  • Diagnosticar as quatro armadilhas mais comuns do código assíncrono a partir do sintoma observado.

Conteúdo​

Por que uma página só sobre isso​

A Aula 3 dedica uma seção a Promises e async/await. Ela é suficiente para ler o código das aulas seguintes. Aqui aprofundamos decisões de escrita e diagnóstico que aparecem nos seguintes casos:

SintomaO que está por trás
A listagem demora 900 ms onde 300 bastariamTrês await em sequência para chamadas independentes
O try/catch não capturou um erro que aconteceuFaltou o await: o bloco terminou antes de o erro chegar
O vetor está vazio depois do laço que o preencheuforEach com callback async: o laço não espera
catch (erro: any) em todo lugarO catch recebe unknown, e a saída fácil apaga a verificação de tipos
Quatro cálculos sob Promise.all demoram o mesmo que em sequênciaTrabalho limitado por CPU: assincronismo sobrepõe espera, não cálculo
O mesmo exemplar foi emprestado duas vezesChecagem e escrita separadas por um await

Esses padrões podem passar pela verificação de tipos; seus efeitos dependem da execução e do tratamento das rejeições.

Como executar os exemplos

Os arquivos completos do projeto extra-a-assincronismo são executáveis; veja a preparação na seção Laboratórios. Os trechos desta exposição são somente leitura: alguns omitem imports e auxiliares, e outros mostram erros intencionais. Use os arquivos completos para executar e comparar resultados. Após instalar as dependências, os exemplos funcionam sem rede, banco ou API. Os tempos de saída são ilustrativos: variam com a máquina e a carga do sistema.


Parte 1 — O modelo de execução​

Três palavras que não são sinônimas​

A conversa do dia a dia trata os três termos como equivalentes, e eles respondem a perguntas diferentes:

TermoO que descrevePergunta que responde
Concorrênciavárias tarefas em andamento no mesmo intervalo, sem exigir que executem no mesmo instantecomo o programa está organizado
Paralelismoduas ou mais tarefas executando no mesmo instante, em núcleos diferentescomo o trabalho é executado
Assincronismoo modelo de programação que inicia a operação, libera a thread e retoma quando o resultado chegacomo se obtém concorrência sem várias threads

Concorrência é lidar com muitas coisas ao mesmo tempo. Paralelismo é fazer muitas coisas ao mesmo tempo.

— Rob Pike, Concurrency is not Parallelism (2012)

No balcão da biblioteca: a atendente que pede um livro ao depósito e, em vez de esperar parada, chama o próximo leitor está agindo de forma assíncrona; os vários pedidos em andamento ao mesmo tempo são a concorrência; uma segunda atendente trabalhando junto é o paralelismo — a única das três que exige mais gente.

As Partes 1 a 4 tratam de concorrência em uma thread só, que é o modelo da stack da disciplina. A Parte 5 mostra onde entra o paralelismo, e por que ele não é o que Promise.all faz.

Uma thread, e o que acontece em volta dela​

Neste material, acompanhamos uma thread de execução de JavaScript: nela, um trecho síncrono executa até terminar antes de outro callback começar. Navegadores e Node.js também podem usar workers e outras threads — assunto da Parte 5.

A pergunta imediata é como um servidor que atende centenas de requisições simultâneas pode ter uma thread só. A resposta é que ele não faz duas coisas ao mesmo tempo — ele espera por várias ao mesmo tempo. Uma espera de entrada e saída não precisa manter essa thread ocupada, embora ainda consuma recursos do ambiente. E é precisamente o tempo de espera que o modelo assíncrono sobrepõe.

Diagrama com quatro caixas e um bloco central. No alto à esquerda, a Pilha de chamadas, azul-escura, descrita como uma só, onde o seu código roda, e que durante a execução síncrona nenhum outro callback dessa thread entra. No alto à direita, as APIs do ambiente, em cinza: temporizadores, rede e disco, que trabalham fora da sua thread. Uma seta azul liga a pilha às APIs, rotulada setTimeout e fetch, com a observação delega e volta na hora. Embaixo à esquerda, a Fila de microtasks, verde, contendo then, catch, await e queueMicrotask. Embaixo à direita, a Fila de macrotasks, azul-clara, contendo setTimeout, setInterval, eventos e conclusões de entrada e saída. Duas setas descem da pilha para a fila de microtasks e das APIs do ambiente para a fila de macrotasks. Ao centro, uma caixa âmbar rotulada Event loop, listando a cada volta, nesta ordem: um, espera a pilha esvaziar; dois, roda todas as microtasks, inclusive as que surgirem no caminho; três, só então tira uma macrotask. Setas âmbar ligam as duas filas a essa caixa e a caixa de volta à pilha de chamadas
Modelo simplificado: microtasks são processadas antes da próxima tarefa. O passo 2 esvazia a fila inteira de microtasks; o passo 3 retira uma única macrotask.

A Figura 1 tem quatro peças e uma regra.

A pilha de chamadas é onde o seu código roda. Enquanto houver função na pilha executando um trecho síncrono, nenhum outro callback assíncrono dessa thread começa, seja de um then ou de um evento de clique. É por isso que um cálculo síncrono demorado pode bloquear a interação com a página, e nenhuma quantidade de async resolve isso — assincronismo resolve espera, não cálculo.

As APIs do ambiente são o que está fora da linguagem: temporizadores, rede, disco, base de dados. Elas trabalham em outro lugar. Quando você chama setTimeout ou fetch, a chamada registra o pedido e retorna na hora — a sua função continua da linha seguinte.

As duas filas guardam o que ficou pronto e está esperando vez. E o event loop é o laço que decide quem entra na pilha, seguindo a regra dos três passos da figura. Microtask é a continuação agendada por mecanismos como then e queueMicrotask; tarefa (task, também chamada macrotask) é trabalho como o callback de um temporizador. A Figura 1 simplifica o ambiente: o navegador tem fontes distintas de tarefas e oportunidades de renderização; o Node tem fases de timers e entrada e saída, além de process.nextTick. Não use o desenho como uma descrição completa da ordem entre essas APIs.

O experimento​

O arquivo src/01-ordem-execucao.ts do projeto de referência tem cinco linhas de saída. Antes de continuar, decida em que ordem elas aparecem:

console.log('1 — síncrono, primeira linha');

setTimeout(() => {
console.log('5 — macrotask (setTimeout com 0 ms)');
}, 0);

void Promise.resolve().then(() => {
console.log('3 — microtask (then de uma promise já resolvida)');
});

queueMicrotask(() => {
console.log('4 — microtask (queueMicrotask)');
});

console.log('2 — síncrono, última linha');

A saída real:

1 — síncrono, primeira linha
2 — síncrono, última linha
3 — microtask (then de uma promise já resolvida)
4 — microtask (queueMicrotask)
5 — macrotask (setTimeout com 0 ms)

Duas regras produzem essa ordem inteira.

As microtasks saem na ordem em que foram enfileiradas, independentemente do mecanismo que as registrou. then e queueMicrotask compartilham a mesma fila neste exemplo; as duas foram enfileiradas durante o trecho síncrono. Um then associado a uma promise ainda pendente só agenda seu callback quando o resultado está disponível, não quando o then é registrado.

Zero milissegundo não significa agora. setTimeout(fn, 0) pede que fn entre na fila de macrotasks assim que possível — e essa fila só é tocada quando a de microtasks está vazia. O timer com zero é a última linha da saída mesmo tendo sido registrado antes das duas microtasks.

A microtask que se multiplica

O passo 2 da figura diz "todas as microtasks, inclusive as que surgirem no caminho". Isso não é detalhe: uma microtask que agenda outra microtask, que agenda outra, prende o event loop para sempre. A fila de macrotasks nunca é alcançada, os timers nunca disparam e a página trava — sem nenhum laço infinito visível no código.


Parte 2 — Do callback à Promise​

Um callback é uma função passada a outra para ser chamada por ela. No padrão usado aqui, o primeiro argumento informa o erro e o segundo, o resultado (Callback<T> está definido no arquivo completo). Antes das Promises, o resultado de uma operação demorada não voltava: era entregue a uma função que você passava junto com o pedido.

function fichaComCallback(id: number, callback: Callback<string>): void {
buscarLivroCb(id, (erro, livro) => {
if (erro || !livro) {
callback(erro ?? new Error('livro ausente'));
return;
}
contarEmprestimosCb(livro.id, (erro2, total) => {
if (erro2 || total === undefined) {
callback(erro2 ?? new Error('total ausente'));
return;
}
callback(null, `${livro.titulo} — ${total} empréstimo(s) em aberto`);
});
});
}

O aninhamento é o sintoma visível, e é o que ficou conhecido como callback hell. Mas o problema de fundo é outro, e tem nome: inversão de controle. Ao passar callback para buscarLivroCb, a sua função entrega a continuação do programa para código de terceiros e depende de que ele a chame — uma vez só, com os argumentos certos, no momento certo. Se a biblioteca chamar duas vezes, o seu programa executa a continuação duas vezes. Se nunca chamar, a continuação não acontece. Uma Promise também pode ficar pendente para sempre; ela não garante conclusão nem tempo limite.

Uma Promise inverte a inversão: a função devolve um objeto que representa o valor futuro, e quem chamou decide o que fazer com ele.

function fichaComPromise(id: number): Promise<string> {
return buscarLivro(id).then((livro) =>
contarEmprestimosAbertos(livro.id).then(
(total) => `${livro.titulo} — ${total} empréstimo(s) em aberto`,
),
);
}

Os três estados​

Diagrama de estados com duas colunas. À esquerda, sob o rótulo ESTADOS, uma caixa cinza chamada pendente, descrita como ainda sem resultado final. Dela saem duas setas curvas: uma verde para cima, rotulada resolve entre parênteses valor comum, chegando à caixa verde realizada, que tem um valor; outra vermelha para baixo, rotulada reject entre parênteses erro, chegando à caixa vermelha rejeitada, que tem um motivo. À direita, sob o rótulo OBSERVADORES, três caixas tracejadas: then de v, que recebe o valor, ligada por seta tracejada à caixa realizada; catch de e, que recebe o motivo, ligada à caixa rejeitada; e finally, que não recebe nada, ligada às duas por setas tracejadas. Abaixo, uma caixa âmbar afirma que liquidada é para sempre: a seta sai de pendente e não tem volta, a promise liquida uma vez só e o resultado fica guardado, observar a mesma promise duas vezes não refaz o trabalho e observá-la tarde demais não perde o valor
Os observadores ficam fora dos estados porque não são estados: then, catch e finally apenas assistem a uma transição que acontece uma vez só.

A Figura 2 resume três propriedades que valem a pena guardar, porque três confusões comuns nascem de ignorá-las.

Neste exemplo, a chamada inicia a busca. buscarLivro(1) executa até seu primeiro await, agendando a espera simulada de 300 ms. O executor passado a new Promise(...) também roda sincronamente. Uma Promise representa um resultado; ela não é, por si só, um trabalho ou uma thread.

Isso não se generaliza a toda API compatível com promises: as consultas do Prisma usadas no curso devolvem PrismaPromise, um objeto thenable (com método then) cuja execução é adiada até ser consumido, por exemplo com await, then ou uma transação.

A transição é única. Uma promise vai de pendente a realizada ou a rejeitada, uma vez, e fica assim. Chamar resolve duas vezes não tem efeito na segunda; observar a mesma Promise nativa em dois lugares não refaz o trabalho. Resolvida não significa necessariamente realizada: resolve(outraPromise) adota o resultado da outra e pode deixar a primeira pendente. Na Figura 2, resolve(valor) representa um valor comum, que não é um thenable. Os estados são pendente (pending), realizada (fulfilled) e rejeitada (rejected); os dois últimos são chamados de liquidada (settled).

then, catch e finally devolvem promises novas. É por isso que se encadeia, e é por isso que o tipo do valor pode mudar no caminho: Promise<Livro> vira Promise<number> quando o callback retorna livro.ano. Um valor retornado realiza a nova promise; uma exceção a rejeita; uma promise retornada tem seu resultado adotado. Um catch que retorna um valor recupera a cadeia. finally normalmente preserva o desfecho anterior, mas, se lançar ou retornar uma promise rejeitada, a nova cadeia rejeita com esse motivo.

0 ms a chamada retornou uma promise pendente — e a busca já começou
306 ms then : Grande Sertão: Veredas
306 ms then : segundo observador da MESMA promise — João Guimarães Rosa
306 ms then : encadeado, o valor mudou de tipo — ano 1956
306 ms finally : roda nos dois desfechos, e não recebe o valor
308 ms catch : LivroNaoEncontradoError — Livro 99 não existe no acervo
308 ms finally : também roda depois de um catch

Repare no segundo observador: ele imprime no mesmo instante que o primeiro, aos 306 ms. A busca aconteceu uma vez.


Parte 3 — async e await​

async/await não é um mecanismo novo. É notação para o que a Parte 2 fez com then e catch, com duas consequências práticas: o fluxo volta a ser lido de cima para baixo, e o try/catch da linguagem volta a funcionar.

Três fatos definem o comportamento.

Toda função async devolve uma Promise. Mesmo quando o corpo retorna um número:

async function anoDePublicacao(id: number): Promise<number> {
const livro = await buscarLivro(id);
return livro.ano; // um number; a função devolve Promise<number>
}

await suspende a função, não o programa. Esta é a confusão mais cara da lista, porque leva a acreditar que assincronismo "trava até chegar". O exemplo abaixo prova o contrário: enquanto ficha espera pela busca de 300 ms, um relógio independente continua batendo a cada 100 ms.

async function ficha(id: number): Promise<string> {
marcar('ficha : entrou na função');
const livro = await buscarLivro(id);
marcar('ficha : voltou do await');
return `${livro.titulo} (${livro.ano})`;
}

async function relogio(): Promise<void> {
for (let batida = 1; batida <= 3; batida += 1) {
await esperar(100);
marcar(`relógio : batida ${batida}`);
}
}

const [texto] = await Promise.all([ficha(1), relogio()]);
0 ms ficha : entrou na função
107 ms relógio : batida 1
212 ms relógio : batida 2
303 ms ficha : voltou do await
314 ms relógio : batida 3
316 ms resultado : Grande Sertão: Veredas (1956)

As duas funções se intercalam. await marca um ponto de retomada: a função é interrompida ali, o resto do programa segue, e ela volta do ponto exato quando o valor chega.

Erro assíncrono vira exceção comum. Diante de um await, uma promise rejeitada lança, e o try/catch captura como capturaria qualquer outra exceção:

async function fichaSegura(id: number): Promise<string> {
try {
const livro = await buscarLivro(id);
return livro.titulo;
} catch (erro) {
if (erro instanceof LivroNaoEncontradoError) {
return `sem ficha (livro ${erro.livroId})`;
}
throw erro;
} finally {
marcar('fichaSegura: finally roda nos dois caminhos');
}
}
O try sem await é uma armadilha silenciosa

Se você apenas chamar uma função que retorna Promise dentro do try, esse catch não captura a rejeição da Promise, mesmo que ela já esteja rejeitada. Use await para tratar a rejeição nesse bloco; também é válido devolver a Promise para o chamador tratá-la ou usar .catch(). Se ninguém tratar, haverá uma rejeição não tratada. Uma exceção síncrona lançada durante a chamada ainda pode ser capturada pelo try. Detalhes na Parte 9.


Parte 4 — Sequencial ou concorrente​

Esta é a parte que mais muda código de aluno, e ela cabe em uma pergunta:

A segunda chamada precisa do resultado da primeira?

Se precisa, existe uma dependência real e a segunda operação deve esperar. Neste exemplo, exigimos confirmar que o livro existe antes de contar:

const livro = await buscarLivro(1);
const total = await contarEmprestimosAbertos(livro.id); // só conta após confirmar a existência

O identificador já era conhecido: sem essa exigência de validação, as duas consultas poderiam ser independentes. Dependências também podem vir de regras de negócio, ordem de efeitos e limites do serviço.

Se não há dependência nem restrição de recursos, a concorrência pode reduzir a latência. As três buscas seguintes são independentes:

const primeiro = await buscarLivro(1);
const segundo = await buscarLivro(2);
const terceiro = await buscarLivro(3);

Cada await só libera a linha seguinte quando termina. O custo é a soma: 300 + 300 + 300.

Linha do tempo com duas faixas, em escala de um centímetro por cem milissegundos. Na faixa de cima, rotulada SEQUENCIAL com await em cada linha, três barras azuis de trezentos milissegundos cada, encostadas uma na outra: buscarLivro de um, de dois e de três, terminando aos novecentos milissegundos, marcados em vermelho por uma linha tracejada. Na faixa de baixo, rotulada CONCORRENTE com await Promise.all, as mesmas três barras em verde, todas começando no zero e terminando aos trezentos milissegundos, marcados em verde. Abaixo, um eixo horizontal graduado de zero a novecentos milissegundos
Nenhuma das seis barras encolheu: cada busca continua custando 300 ms. O que mudou foi a sobreposição das esperas.

A Figura 3 deixa explícito o que a medição confirma:

912 ms sequencial : 3 livros
309 ms Promise.all : 3 livros

Duas leituras erradas de Promise.all valem ser desfeitas.

Ele não chama as funções por você. Neste exemplo, as operações do vetor já estavam em andamento quando Promise.all foi chamado — quem disparou foi a chamada da função. Promise.all acompanha os resultados; ao receber thenables como os do Prisma, ele os consome, o que pode iniciar as operações adiadas.

Ele não cria threads. Continua havendo uma linha de execução. O que se sobrepõe é a espera, que é onde o tempo estava sendo gasto — a diferença entre isso e paralelismo de verdade é o assunto da Parte 5.

O mesmo erro dentro de um laço​

É a forma mais comum de encontrá-lo, e a mais fácil de não enxergar:

// Versão sequencial (~1200 ms para quatro livros)
const ids = [1, 2, 3, 4];
const titulosSequenciais: string[] = [];
for (const id of ids) {
const livro = await buscarLivro(id);
titulosSequenciais.push(livro.titulo);
}

// 306 ms para os mesmos quatro
const titulos = await Promise.all(ids.map(async (id) => (await buscarLivro(id)).titulo));
Concorrência sem teto também é defeito

Promise.all sobre quinhentos ids solicita quinhentas operações de uma vez. Isso pode sobrecarregar o serviço; não implica quinhentas conexões, pois pools e protocolos podem limitar ou compartilhar conexões. O padrão para isso é o processamento em lotes, na Parte 8.


Parte 5 — Concorrência não é paralelismo​

A Parte 4 trocou 900 ms por 300 ms sem que nenhuma busca ficasse mais rápida: o ganho veio de sobrepor espera. Isso sugere uma pergunta anterior à da Parte 4:

O tempo está sendo gasto esperando ou calculando?

Uma tarefa limitada por entrada e saída (I/O-bound) passa a maior parte do tempo aguardando rede, disco ou base de dados: a thread fica livre, e é aí que o assincronismo trabalha. Uma tarefa limitada por CPU (CPU-bound) passa o tempo calculando: a thread fica ocupada, e não existe espera para sobrepor.

TarefaOnde o tempo vaiFerramenta
Consultar o acervo no PostgreSQLesperaawait, Promise.all
Chamar o catálogo externo de capasesperaPromise.all, allSettled
Ler um arquivo grande de importaçãoesperaiteração assíncrona (Parte 8)
Gerar miniaturas das capas enviadascálculooutra thread
Montar o índice de busca do acervocálculooutra thread
Produzir o relatório mensal em PDFcálculooutra thread

O experimento: cálculo em vez de espera​

calcularAssinatura, em src/assinatura.worker.ts, é um laço de multiplicações calibrado para levar cerca de 300 ms — e não espera por nada. O arquivo src/11-concorrencia-paralelismo.ts produz quatro assinaturas de três formas e conta, em cada uma, quantas vezes um relógio de 50 ms conseguiu bater:

async function assinaturaAsync(id: number): Promise<number> {
return calcularAssinatura(id, RODADAS); // nenhum await: nada suspende
}

const assinaturas = await Promise.all(ids.map((id) => assinaturaAsync(id)));

A saída em uma máquina de quatro núcleos:

núcleos disponíveis: 4
1215 ms sequencial : 4 assinaturas — relógio bateu 0 vez(es)
1211 ms Promise.all: 4 assinaturas — relógio bateu 0 vez(es)
437 ms 4 workers : 4 assinaturas — relógio bateu 8 vez(es)
mesmas assinaturas nas três versões: true
Linha do tempo com três faixas, em escala de um centímetro por cem milissegundos, sobre um eixo comum de zero a mil e duzentos milissegundos. Na primeira faixa, cálculo em uma thread com await Promise.all, quatro barras vermelhas de preenchimento cheio, de trezentos milissegundos cada, encostadas uma na outra: assinatura um, dois, três e quatro, terminando aos mil e duzentos milissegundos, marcados em vermelho. Na segunda faixa, cálculo em quatro workers, uma thread para cada assinatura: quatro linhas, cada uma com uma barra cinza curta de cento e quarenta milissegundos rotulada criação, seguida de uma barra verde de trezentos milissegundos; todas terminam aos quatrocentos e quarenta milissegundos, marcados em verde por uma linha tracejada que para dentro do próprio painel. Na terceira faixa, espera em uma thread com o mesmo Promise.all sobre entrada e saída, três barras azuis de contorno tracejado, buscarLivro de um, de dois e de três, todas começando no zero e terminando aos trezentos milissegundos, marcados em azul
Preenchimento cheio marca processador ocupado; contorno tracejado, espera fora da thread. Nas duas primeiras faixas o trabalho é o mesmo, e Promise.all não tem o que sobrepor.

Promise.all não economizou nada, e nas duas primeiras versões o relógio não bateu uma única vez em mais de um segundo. O motivo está na Parte 3: o corpo de uma função async roda de forma síncrona até o primeiro await, e aqui não há nenhum. Cada chamada calcula a assinatura inteira antes de devolver a promise, então Promise.all recebe quatro promises já liquidadas — é a armadilha 4 da Parte 9 em tamanho real. A Figura 4 mostra as três faixas na mesma escala: só na terceira, onde o tempo é espera, existe algo a sobrepor.

Quando a ferramenta é outra thread​

import { Worker } from 'node:worker_threads';

function assinaturaEmWorker(id: number): Promise<number> {
return new Promise((resolve, reject) => {
const worker = new Worker(new URL('./assinatura.worker.ts', import.meta.url), {
workerData: { livroId: id, rodadas: RODADAS },
});
worker.once('message', (valor: unknown) => {
if (typeof valor === 'number') resolve(valor);
else reject(new TypeError('o worker devolveu um valor que não é número'));
});
worker.once('error', reject);
worker.once('exit', (codigo) => {
if (codigo !== 0) reject(new Error(`worker terminou com código ${codigo}`));
});
});
}

Três observações sobre esse trecho.

A interface continua sendo uma Promise. Tudo o que as Partes 4 e 6 dizem sobre combinar promises vale aqui sem mudança; o que mudou é onde o trabalho roda. Envolver o worker em uma promise é o mesmo padrão da Parte 2, aplicado a uma API de eventos.

O worker não compartilha memória. Ele recebe uma cópia dos dados em workerData e devolve outra cópia por mensagem — o que atravessa é clonado, não referenciado. É esse isolamento que dispensa travas neste exemplo. Existe forma de compartilhar memória de fato entre threads, com SharedArrayBuffer, e ela fica fora do recorte desta página.

Criar thread custa. Os 437 ms medidos, contra os 300 ms de uma assinatura sozinha, são o preço de criar quatro workers e trocar mensagens com eles. Para tarefas curtas o custo pode superar o ganho; quando há muitas, o padrão é reaproveitar um conjunto fixo de workers em vez de criar um por item — a mesma ideia do teto de concorrência da Parte 8.

O Node já usa threads sem você pedir

Algumas operações assíncronas do próprio Node rodam em um thread pool mantido pelo libuv: parte de fs, dns.lookup, zlib e as funções assíncronas de crypto. O seu código JavaScript continua em uma thread só — o que corre fora dela é o trabalho nativo dessas APIs.

A Aula 10 tem um caso dos dois lados. O bcryptjs é JavaScript puro e, nas funções assíncronas, divide o cálculo em pedaços que voltam para o fim da fila do event loop entre um e outro, cedendo vez ao resto do programa — concorrência, sem paralelismo. Já o pacote nativo bcrypt calcula no thread pool: "the async version uses a thread pool which does not block the main event loop", diz a documentação dele. Mesma tarefa, dois modelos de execução.

O teto do ganho: a Lei de Amdahl​

Nem todo programa acelera na proporção dos núcleos. Se uma fração pp do tempo total pode ser paralelizada e o resto é inerentemente sequencial, o ganho máximo com NN processadores é

S(N)=1(1−p)+pNS(N) = \frac{1}{(1 - p) + \dfrac{p}{N}}
Fração paralelizável2 núcleos4 núcleos8 núcleosInfinitos
50%1,33 vez1,60 vez1,78 vez2 vezes
90%1,82 vez3,08 vezes4,71 vezes10 vezes
99%1,98 vez3,88 vezes7,48 vezes100 vezes

A leitura prática: com metade do tempo em trecho sequencial, nem infinitos núcleos passam de duas vezes. É por isso que medir vem antes de paralelizar — e é a mesma razão pela qual o experimento acima parou em 2,8 vezes com quatro núcleos, e não em 4: criar os workers e trocar mensagens é a parte que não paraleliza.

Uma thread só não elimina condição de corrida​

O que uma thread garante é que um trecho síncrono não é interrompido. Ela não garante nada entre dois trechos: cada await é um ponto em que outra chamada pode entrar, ler o mesmo estado e decidir com base nele. É a condição de corrida — duas execuções concorrentes cujo resultado depende de quem chega primeiro.

O arquivo src/12-corrida-logica.ts empresta o mesmo livro duas vezes:

async function emprestarComCorrida(livroId: number, leitor: string): Promise<string> {
if (situacoes.get(livroId) !== 'DISPONIVEL') {
return `${leitor}: recusado`;
}
await validarLeitor(leitor); // a thread atende a outra chamada aqui
situacoes.set(livroId, 'EMPRESTADO');
registros.push(`livro ${livroId} para ${leitor}`);
return `${leitor}: emprestado`;
}

await Promise.all([emprestarComCorrida(4, 'ana'), emprestarComCorrida(4, 'bruno')]);
104 ms com corrida : ana: emprestado | bruno: emprestado
105 ms com corrida : registros = [livro 4 para ana; livro 4 para bruno]

As duas chamadas verificaram a disponibilidade antes de qualquer uma gravar. A correção não é uma trava: é não deixar espera entre a decisão e a marca.

if (situacoes.get(livroId) !== 'DISPONIVEL') {
return `${leitor}: recusado`;
}
situacoes.set(livroId, 'RESERVADO'); // nenhum callback entra entre a checagem e esta linha

try {
await validarLeitor(leitor);
} catch (erro) {
situacoes.set(livroId, 'DISPONIVEL'); // desfaz a reserva
throw erro;
}

situacoes.set(livroId, 'EMPRESTADO');
209 ms sem corrida : ana: emprestado | bruno: recusado
209 ms sem corrida : registros = [livro 4 para ana]
Na aplicação real, essa memória não é compartilhada

O trecho síncrono resolve o problema dentro de um processo. Duas instâncias da API, dois workers ou dois processos não compartilham esse Map, e a janela reabre. Quando o estado está no banco, quem garante a exclusão é o banco: é o assunto do aviso "transação garante atomicidade, não exclusão mútua" da Aula 9, cuja saída é uma escrita condicional, que só grava se o empréstimo ainda estiver em aberto.

Três afirmações que valem desfazer​

Afirmação comumO que de fato acontece
"async é paralelo"async organiza espera. Sem outra thread, dois cálculos não correm no mesmo instante
"mais threads sempre acelera"Acelera cálculo independente com núcleo livre. Para espera, acrescenta custo sem ganho
"uma thread só não tem condição de corrida"Não há corrida por memória, mas a corrida entre dois await continua possível

Como os três aparecem juntos​

Um serviço como o do Módulo 2 usa os três ao mesmo tempo: o event loop atende muitas requisições concorrentes enquanto elas aguardam o banco; o thread pool do ambiente cuida das operações nativas; e o trabalho pesado de CPU — gerar um PDF, processar uma imagem — sai da thread principal para um worker ou para um processo separado. A pergunta útil não é qual dos três é melhor, e sim qual deles corresponde ao gargalo que você mediu.

Quatro perguntas, nesta ordem:

  1. O tempo vai em espera ou em cálculo? Meça antes de mudar o código.
  2. As tarefas dependem umas das outras? É a pergunta da Parte 4.
  3. Elas decidem sobre o mesmo estado? Se sim, procure a janela entre a decisão e a escrita.
  4. Quanto do trabalho é de fato paralelizável? Amdahl limita o resto.

Parte 6 — Falha e demora não são exceção​

Mesmo com o tratamento de erros estudado nas aulas, é preciso prever falhas parciais e diferenças de tempo entre operações. Em produção, uma das fontes está fora do ar, outra responde em oito segundos, e a terceira responde certo. Esta parte trata desse caso — que é o caso normal.

O cenário é o mesmo nos quatro exemplos: a ficha de um livro pode vir de três fontes, e a mais rápida delas é justamente a que está quebrada.

FonteTempoDesfecho
catálogo do parceiro100 msrejeita
acervo local250 msresponde
catálogo nacional400 msresponde
Linha do tempo em escala de um centímetro por cinquenta milissegundos, dividida em duas faixas. Na faixa de cima, AS TRÊS FONTES DA FICHA: uma barra vermelha curta do catálogo do parceiro, que rejeita aos cem milissegundos; uma barra verde do acervo local, que responde aos duzentos e cinquenta; uma barra azul do catálogo nacional, que responde aos quatrocentos. Na faixa de baixo, QUANDO CADA COMBINADOR LIQUIDA: Promise.all em vermelho até os cem milissegundos, rejeitando com o erro do parceiro enquanto as outras duas continuam em andamento, sem alterar o resultado de all; Promise.allSettled em verde até os quatrocentos, com três resultados, duas respostas e uma falha; Promise.race em vermelho até os cem, rejeitando com a primeira notícia, que é má; Promise.any em verde até os duzentos e cinquenta, com a resposta do acervo local, ignorando a rejeição. Linhas tracejadas verticais ligam o fim de cada barra ao eixo de tempo embaixo
Com a fonte mais rápida quebrada, race e any dão respostas opostas — é esse arranjo que separa os quatro combinadores.

Como mostra a Figura 5, a escolha entre os quatro é a escolha de um desfecho:

CombinadorUse quandoLiquida
Promise.allprecisa de todas; qualquer falha invalida o conjuntona primeira rejeição, ou quando a última realizar
Promise.allSettledprecisa do relatório completo, sucesso e falhaquando todas liquidarem; rejeições de entrada viram registros
Promise.raceprecisa da primeira notícia, seja qual forna primeira que liquidar, inclusive rejeitando
Promise.anyprecisa da primeira resposta boana primeira que realizar; rejeita só se todas falharem
106 ms all : rejeitou — catálogo do parceiro fora do ar
407 ms allSettled : esperou todas — 2 respostas, 1 falha(s)
107 ms race : rejeitou — catálogo do parceiro fora do ar
261 ms any : acervo local

Duas observações que a tabela não mostra.

Promise.all rejeita cedo, mas não cancela nada. Aos 100 ms ele já rejeitou; as outras duas buscas continuam correndo até o fim, consumindo recursos. all mantém os tratadores associados às entradas, mas os resultados posteriores não alteram sua rejeição nem desfazem efeitos já realizados.

Quando todas falham, Promise.any rejeita com um AggregateError, que carrega a lista completa em .errors. É o único combinador que devolve um erro composto por esse motivo. all e allSettled preservam a ordem de entrada, não a ordem de conclusão. Para um vetor vazio, all e allSettled realizam com [], any rejeita com AggregateError e race permanece pendente. A tabela considera arrays válidos; erros ao percorrer uma entrada inválida também podem rejeitar allSettled.

Tempo limite: desistir de esperar​

O padrão mais simples é uma corrida entre a tarefa e um temporizador:

async function comTempoLimite<T>(tarefa: Promise<T>, ms: number): Promise<T> {
let timer: ReturnType<typeof setTimeout> | undefined;
const estouro = new Promise<never>((_, reject) => {
timer = setTimeout(() => reject(new TempoEsgotadoError(ms)), ms);
});
try {
return await Promise.race([tarefa, estouro]);
} finally {
clearTimeout(timer); // libera o timer se a tarefa terminar primeiro
}
}

Funciona, e resolve metade do problema. A sua função para de esperar — mas a tarefa original continua correndo até o fim, porque ninguém a avisou.

Cancelamento: avisar quem trabalha​

AbortSignal é o mecanismo padrão para isso, e é o mesmo que fetch recebe em { signal } — o que torna esta seção diretamente aplicável ao cliente web do Módulo 3.

function tarefaLonga(ms: number, signal: AbortSignal): Promise<string> {
return new Promise((resolve, reject) => {
signal.throwIfAborted();
const cancelar = () => {
clearTimeout(id);
reject(signal.reason); // o motivo pode ser qualquer valor
};
const id = setTimeout(() => {
signal.removeEventListener('abort', cancelar);
resolve(`terminei em ${ms} ms`);
}, ms);
signal.addEventListener('abort', cancelar, { once: true });
});
}

try {
await tarefaLonga(400, AbortSignal.timeout(150));
} catch (erro) {
console.log(erro instanceof Error ? erro.name : 'cancelada');
}
155 ms race : tempo limite de 150 ms esgotado
310 ms AbortSignal: TimeoutError — o timer foi cancelado

Os tempos acima são acumulados: o segundo teste começa depois do primeiro e também espera cerca de 150 ms. O cancelamento é cooperativo: a operação precisa observar o sinal e liberar recursos. Abortar um fetch não garante que o servidor desfaça uma escrita já recebida. Neste simulador, cancelamos o temporizador que representa o trabalho.

A diferença entre as duas linhas é o ponto da seção: Promise.race resolve o problema de quem espera; AbortSignal resolve o problema de quem trabalha.

Repetir, com recuo​

async function comRepeticao<T>(
tarefa: () => Promise<T>,
tentativas: number,
esperaInicial: number,
): Promise<T> {
if (!Number.isInteger(tentativas) || tentativas < 1) {
throw new RangeError('tentativas deve ser um inteiro positivo');
}
if (!Number.isFinite(esperaInicial) || esperaInicial < 0) {
throw new RangeError('esperaInicial deve ser finita e não negativa');
}
let ultimoErro: unknown;
for (let tentativa = 1; tentativa <= tentativas; tentativa += 1) {
try {
return await tarefa();
} catch (erro) {
if (!(erro instanceof CatalogoIndisponivelError)) throw erro;
ultimoErro = erro;
if (tentativa === tentativas) break;
const espera = esperaInicial * 2 ** (tentativa - 1); // 100, 200, 400...
console.log(` repetindo em ${espera} ms (tentativa ${tentativa} falhou)`);
await esperar(espera);
}
}
throw new Error(`falhou após ${tentativas} tentativas`, { cause: ultimoErro });
}

CatalogoIndisponivelError é o erro de infraestrutura definido em biblioteca.ts; neste simulador, só ele autoriza repetir. O recuo dobra a espera entre tentativas para evitar insistir imediatamente.

Note dois detalhes. O parâmetro é uma função que devolve promise, e não uma promise: uma promise já liquidada não pode ser refeita (Parte 2), então repetir exige poder chamar de novo. E o erro final carrega cause, preservando o erro original em vez de apagá-lo.

Repetir só faz sentido para falha transitória

Tempo esgotado, conexão recusada, indisponibilidade momentânea: repetir tem chance de dar certo. Um "livro não encontrado" ou um "dados inválidos" vai falhar exatamente igual nas quatro tentativas — e você terá gasto quatro vezes mais tempo para receber a mesma resposta correta. Antes de repetir uma escrita, verifique se ela é idempotente (repeti-la preserva o efeito de uma única execução), conceito da Aula 4. Um tempo limite não prova que a primeira tentativa deixou de gravar dados.


Parte 7 — Tipar o assíncrono​

Esta é a parte que faz a página ser de TypeScript, e não de JavaScript.

O que await desembrulha​

await acompanha recursivamente a resolução de promises e thenables até obter o valor final. O utilitário Awaited<T> modela esse mesmo comportamento no sistema de tipos, inclusive quando o tipo descreve promises aninhadas:

async function anoDoLivro(id: number): Promise<number> {
const livro: Livro = await buscarLivro(id); // Promise<Livro> -> Livro
return livro.ano;
}

type RetornoBruto = ReturnType<typeof anoDoLivro>; // Promise<number>
type RetornoUtil = Awaited<RetornoBruto>; // number

O erro correspondente é frequente e o compilador o pega:

// Type 'Promise<number>' is not assignable to type 'number'.
const erroDeTipo: number = anoDoLivro(1);

Genéricas atravessam a espera​

async function primeiroQueResponder<T>(tarefas: Array<Promise<T>>): Promise<T> {
return Promise.any(tarefas);
}

const livro = await primeiroQueResponder([buscarLivro(1), buscarLivro(2)]);
console.log(livro.titulo); // o compilador sabe que é Livro, não any

O catch recebe unknown, e isso é uma boa notícia​

Em JavaScript qualquer valor pode ser lançado: throw 'texto' é legal. Por isso a opção useUnknownInCatchVariables — que faz parte do strict — tipa o parâmetro do catch como unknown, e obriga a provar o que ele é:

catch (erro) {
console.log(erro.message); // erro: 'erro' is of type 'unknown'
}

A saída fácil é catch (erro: any), e ela apaga a verificação justamente no caminho de erro — o menos testado do programa. A saída correta é uma função de uma linha, reaproveitada em todo lugar:

export function mensagemDeErro(erro: unknown): string {
if (erro instanceof Error) return erro.message;
if (typeof erro === 'string') return erro;
return 'erro desconhecido';
}

Dado que veio de fora não tem tipo​

Este é o ponto mais importante da parte, e o que mais se repete no Módulo 3. resposta.json() devolve any. Aceitar esse any apaga a verificação de tipos exatamente na única fronteira em que o dado não foi escrito por você:

// o tipo é uma promessa sua, não uma garantia do compilador
const livro = await resposta.json() as Livro;

Receba como unknown e valide com um type guard: uma função que verifica o valor em execução e permite ao compilador estreitar seu tipo. Este guarda confere a estrutura, não regras de domínio como ISBN válido ou ano permitido:

function ehLivro(valor: unknown): valor is Livro {
if (typeof valor !== 'object' || valor === null) return false;
const candidato = valor as Record<string, unknown>;
return (
typeof candidato.id === 'number' &&
typeof candidato.titulo === 'string' &&
typeof candidato.autor === 'string' &&
typeof candidato.ano === 'number' &&
typeof candidato.isbn === 'string'
);
}

async function lerLivroDaApi(carga: string): Promise<Livro> {
const dado = await receberDaApi(carga); // receberDaApi devolve Promise<unknown>
if (!ehLivro(dado)) {
throw new TypeError('resposta fora do contrato de Livro');
}
return dado; // a partir daqui, e só a partir daqui, é Livro
}
Onde você já viu isso

É a mesma ideia do ValidationPipe com class-validator da Aula 6: o tipo descreve o que se espera, e a validação em tempo de execução confere se chegou. As duas camadas se completam — uma não substitui a outra.


Parte 8 — Iteração assíncrona​

Até aqui, todo conjunto de valores estava disponível de uma vez. Nem sempre está: uma listagem paginada, uma leitura de arquivo grande, uma resposta que chega em pedaços — todas produzem valores ao longo do tempo.

for await...of​

const pendentes = [1, 2, 3].map((id) => buscarLivro(id));
for await (const livro of pendentes) {
marcar(`for await : ${livro.titulo}`);
}
306 ms for await : Grande Sertão: Veredas
307 ms for await : Memórias Póstumas de Brás Cubas
307 ms for await : Vidas Secas

Repare no tempo: os três saem juntos, pouco depois dos 300 ms. As chamadas foram disparadas na criação do vetor, então correm concorrentes; o laço apenas consome em ordem. Isso é diferente de um laço que chama a função a cada volta, que é o caso lento da Parte 4.

Promises já iniciadas podem rejeitar antes da sua vez

O laço aguarda uma entrada por vez. Se uma entrada posterior rejeitar enquanto a primeira ainda está pendente, pode surgir uma rejeição não tratada. Para um conjunto já iniciado que pode falhar, prefira Promise.all ou Promise.allSettled, que associam tratadores a todas as entradas imediatamente.

Geradores assíncronos​

Um gerador async é uma fonte que produz valores sob demanda. É o formato natural para paginação, porque o consumidor não precisa saber quantas páginas existem:

async function* paginarAcervo(tamanhoDaPagina: number): AsyncGenerator<Livro[]> {
if (!Number.isInteger(tamanhoDaPagina) || tamanhoDaPagina < 1) {
throw new RangeError('tamanhoDaPagina deve ser um inteiro positivo');
}
let pagina = 1;
while (true) {
const ids = idsDaPagina(pagina, tamanhoDaPagina);
if (ids.length === 0) return;
yield Promise.all(ids.map((id) => buscarLivro(id)));
pagina += 1;
}
}

for await (const paginaDeLivros of paginarAcervo(2)) {
marcar(`página : ${paginaDeLivros.map((livro) => livro.id).join(', ')}`);
}

O consumidor recebe uma página por vez. Numa API realmente paginada, isso evita carregar todo o acervo, desde que ele não acumule os resultados. Aqui, idsDaPagina e o acervo em memória apenas simulam essa API.

Lotes: concorrência com teto​

A resposta para o aviso da Parte 4. Nem tudo de uma vez, nem um de cada vez:

async function emLotes<T, R>(
itens: readonly T[],
tamanhoDoLote: number,
tarefa: (item: T) => Promise<R>,
): Promise<R[]> {
if (!Number.isInteger(tamanhoDoLote) || tamanhoDoLote < 1) {
throw new RangeError('tamanhoDoLote deve ser um inteiro positivo');
}
const resultados: R[] = [];
for (let i = 0; i < itens.length; i += tamanhoDoLote) {
const lote = itens.slice(i, i + tamanhoDoLote);
resultados.push(...(await Promise.all(lote.map(tarefa))));
}
return resultados;
}

Quatro itens em lotes de dois custam duas rodadas de 300 ms, e não uma de 300 nem quatro de 300. O teto é seu; a escolha do número depende do que está do outro lado. Cada lote espera seu item mais lento antes de iniciar o próximo. Se um item rejeitar, a função rejeita e não inicia novos lotes; as operações já iniciadas continuam. Esta função acumula todos os resultados em memória.


Parte 9 — Quatro armadilhas​

Estes padrões podem compilar sem erro. Os três primeiros podem produzir falhas; o quarto costuma indicar uma expectativa incorreta sobre a API.

1. forEach com callback async​

const titulos: string[] = [];

[1, 2, 3].forEach(async (id) => {
const livro = await buscarLivro(id);
titulos.push(livro.titulo);
});

marcar(`forEach : ${titulos.length} títulos — o vetor ainda está vazio`);

forEach ignora o valor de retorno do callback. Como o callback é async, ele devolve uma promise — que forEach descarta. O laço termina imediatamente, o vetor ainda está vazio, e as três buscas continuam correndo sozinhas.

Correção: map devolve as promises, e Promise.all espera por elas.

const titulos = await Promise.all([1, 2, 3].map(async (id) => (await buscarLivro(id)).titulo));

As duas versões, lado a lado, na saída real:

0 ms forEach : 0 títulos — o vetor ainda está vazio
303 ms map/all : 3 títulos

2. try/catch sem await​

try {
buscarLivro(99); // sem await
} catch {
// nunca roda
}

O try cria a promise e termina. A rejeição chega 300 ms depois, quando o bloco já saiu de cena. Correção: await buscarLivro(99) dentro do try.

3. Promise solta​

Uma chamada sem await e sem catch é uma rejeição esperando para acontecer em outro lugar:

salvarEmprestimo(404, 'ana'); // devolve Promise; ninguém observa

Se rejeitar sem tratamento, o Node pode emitir unhandledRejection; na configuração padrão, sem tratador, isso termina o processo. Flags e tratadores globais podem alterar o comportamento. O arquivo demonstrativo instala um tratador global apenas para exibir a falha e continuar; isso não substitui tratar cada operação na aplicação.

Correção: ou await, ou um .catch() explícito. Se a intenção é mesmo não esperar, marque com void e trate o erro:

void salvarEmprestimo(404, 'bruno').catch((erro: unknown) => {
console.log(`[tratada] ${mensagemDeErro(erro)}`);
});

void apenas descarta o valor da expressão: não trata rejeições. O tratamento, aqui, é feito pelo .catch().

4. await no que não é promise​

const numero = await 42; // válido: a continuação ainda é assíncrona

Não é erro: a continuação é agendada como microtask, mesmo com um valor comum. Isso pode ser intencional. A armadilha é supor que await transforma uma função síncrona demorada em trabalho que não bloqueia a thread: a chamada é avaliada antes da espera e continua bloqueando enquanto executa.

Sintoma observadoArmadilha provável
Vetor vazio logo depois do laço que o preenche1 — forEach com async
O catch não capturou um erro que aconteceu2 — falta de await no try
O processo terminou por uma rejeição não tratada3 — promise solta
O cálculo bloqueia mesmo quando a chamada usa await4 — await no que não é promise

Parte 10 — Onde isso já apareceu no semestre​

Estas conexões situam o aprofundamento no percurso da disciplina:

OndeO que era, de fato
LivrosService.listar() da Aula 6Retorna dados síncronos do acervo em memória; usar NestJS não obriga a usar async
Toda chamada ao Prisma na Aula 7PrismaPromise: consultas com execução adiada, consumidas por await ou compostas em uma transação
As transações da Aula 9Garantem atomicidade no banco: em caso de falha, desfazem alterações da transação. Promise.all não oferece rollback
O componente de servidor da Aula 11Um componente async: o React aguarda a promise antes de renderizar
O params assíncrono do Next.js 16params deve ser aguardado antes do acesso aos campos; tipos e runtime podem diagnosticar o acesso incorreto
O hash de senha da Aula 10Cálculo, não espera: o bcryptjs divide o trabalho em pedaços na mesma thread, cedendo vez entre eles
Future com async/await no Dart, no Módulo 4Conceitos semelhantes de resultado futuro e suspensão da função, com APIs e regras próprias da linguagem

No Dart, cada isolate tem seu próprio fluxo de execução e event loop; isolates rodam em paralelo e trocam mensagens em vez de compartilhar memória, como os workers da Parte 5. A analogia ajuda a estudar Future, mas não substitui a documentação da linguagem.


Laboratórios​

Os checkpoints da próxima seção verificam compreensão. Estes laboratórios verificam prática: são exercícios para digitar, rodar e ver quebrar. São seis, progressivos, e cabem em uma ou duas sessões.

Preparando o ambiente​

Crie a pasta extra-a-assincronismo, com as subpastas src e praticar. Baixe os arquivos individualmente pelos links e preserve seus nomes e pastas. biblioteca.ts fornece os dados, erros e temporizadores compartilhados. Os doze exemplos em src estão completos; os seis exercícios em praticar contêm tarefas indicadas por TODO.

Arquivo para downloadPasta de destino
package.jsonraiz
tsconfig.jsonraiz
tsconfig.praticar.jsonraiz
README.mdraiz
src/01-ordem-execucao.tssrc
src/02-callback-para-promise.tssrc
src/03-promises.tssrc
src/04-async-await.tssrc
src/05-sequencial-concorrente.tssrc
src/06-combinadores.tssrc
src/07-tempo-limite-repeticao.tssrc
src/08-tipagem.tssrc
src/09-iteracao-assincrona.tssrc
src/10-armadilhas.tssrc
src/11-concorrencia-paralelismo.tssrc
src/12-corrida-logica.tssrc
src/assinatura.worker.tssrc
src/biblioteca.tssrc
praticar/01-ordem-execucao.tspraticar
praticar/02-ficha-do-livro.tspraticar
praticar/03-sequencial-concorrente.tspraticar
praticar/04-combinadores.tspraticar
praticar/05-tempo-limite.tspraticar
praticar/06-tipagem.tspraticar
praticar/LEIA-ME.mdpraticar

Na pasta extra-a-assincronismo, execute:

npm install
npm run check
npx tsx src/01-ordem-execucao.ts
ComandoO que faz
npm run checkverifica os tipos dos exemplos de src
npm run check:praticarverifica também os exercícios de praticar
npx tsx src/NN-arquivo.tsexecuta um exemplo; substitua pelo nome real
npx tsx praticar/NN-arquivo.tsexecuta o seu exercício

A instalação requer internet; depois, os programas funcionam sem rede, PostgreSQL ou API. tsx executa TypeScript, mas não verifica tipos: por isso os comandos de verificação são uma etapa separada. Os arquivos usam ESM ("type": "module"), com module e moduleResolution em nodenext e strict: true, como no ambiente Node da Aula 3. Os imports locais usam .js para corresponder à extensão após compilação.

Ambiente sugerido

Use Node.js 22 ou 24. Os downloads fixam TypeScript 6.0.3 e as versões das ferramentas no package.json; não exigem TypeScript 7. Esta revisão foi verificada com Node.js 22.23.2 e Node.js 24.21.0, ambos com TypeScript 6.0.3. As APIs usadas incluem Promise.any, AbortSignal.timeout, Error.cause e node:worker_threads. O exemplo 11 mede tempos que dependem do número de núcleos da sua máquina, e aponta para um arquivo .ts ao criar o worker porque o projeto roda com tsx; em um projeto compilado, o caminho seria o .js gerado.

Laboratório 1 — Prever antes de rodar​

Arquivo: praticar/01-ordem-execucao.ts · Tempo: ~15 min

Escreva a ordem de saída esperada antes de executar. Depois rode e compare. Se errar, identifique qual das duas regras da Parte 1 você não aplicou.

Em seguida, acrescente um queueMicrotask depois da última linha síncrona e responda, antes de rodar, se ele sai antes ou depois da microtask que já existia. Por fim, faça o setTimeout de 0 ms sair depois do de 10 ms sem alterar nenhum dos dois números: agende o timer de 0 ms dentro do callback do timer de 10 ms. Trocar apenas a posição das duas chamadas não garante a ordem.

Laboratório 2 — Converter callback em await​

Arquivo: praticar/02-ficha-do-livro.ts · Tempo: ~20 min

A versão em estilo de callback está pronta. Escreva a equivalente com async/await, trate o livro inexistente devolvendo um texto em vez de propagar o erro, e implemente fichas(ids) para vários livros.

Antes de escrever fichas, responda em comentário: as buscas dependem umas das outras? A resposta escolhe entre laço com await e Promise.all — e é o assunto do laboratório seguinte.

Laboratório 3 — Medir a diferença​

Arquivo: praticar/03-sequencial-concorrente.ts · Tempo: ~25 min

Escreva relatorio(ids) primeiro na versão ingênua, com dois await dentro de um laço, e meça. Exija que a existência do livro seja confirmada antes de contar seus empréstimos. Depois processe livros diferentes concorrentemente, preservando essa ordem dentro de cada ficha, e aguarde o conjunto com Promise.all. Marque em comentário de onde vem a dependência.

Para quatro livros, espere aproximadamente 4 × (300 + 250) = 2200 ms na versão sequencial e 550 ms na concorrente. Repita a medição e investigue diferenças grandes: podem vir do código ou da carga da máquina.

Laboratório 4 — Escolher o combinador​

Arquivo: praticar/04-combinadores.ts · Tempo: ~25 min

Quatro situações, quatro combinadores. Decida antes de codificar qual usar em cada uma e por quê; depois implemente e cronometre as quatro.

Explique por que uma delas rejeita aos 100 ms sem esperar pelas fontes que dariam certo. Ao final, faça as três fontes falharem e observe qual delas passa a rejeitar com um AggregateError.

Laboratório 5 — Tempo limite e cancelamento​

Arquivo: praticar/05-tempo-limite.ts · Tempo: ~30 min

Implemente comTempoLimite com Promise.race e prove que a tarefa original não foi cancelada: faça-a imprimir uma linha ao terminar e observe essa linha aparecer depois do erro de tempo.

Depois escreva a versão com AbortSignal que realmente interrompe o trabalho e confirme que a linha desapareceu. Por fim, implemente a repetição com recuo contra o catálogo externo, que falha nas duas primeiras chamadas.

Laboratório 6 — Tipar sem any​

Arquivo: praticar/06-tipagem.ts · Tempo: ~25 min

Declare o tipo de retorno da função assíncrona, construa Awaited<ReturnType<...>> e confirme que é string. Escreva mensagemDeErro(erro: unknown) tratando três formas, e o guarda ehLivro(valor: unknown): valor is Livro para validar um JSON completo e um JSON incompleto.

Execute npm run check:praticar para verificar os tipos do exercício e rode o arquivo para confirmar que o guarda aceita o JSON completo e rejeita o incompleto. Tipagem correta não prova a validação em tempo de execução.

Critérios de conclusão​

Os laboratórios estão completos quando:

  • você previu corretamente a ordem de saída do laboratório 1 e sabe justificar cada posição;
  • a versão com async/await do laboratório 2 produz o mesmo texto da versão com callback;
  • a versão concorrente do laboratório 3 é mensuravelmente mais rápida, e você sabe apontar o await que não pôde ser removido;
  • as quatro implementações do laboratório 4 produzem os desfechos e tempos aproximados previstos pela Figura 5;
  • no laboratório 5, a versão com AbortSignal deixa de imprimir a linha que a versão com race ainda imprimia;
  • npm run check:praticar passa no laboratório 6 sem nenhum any e sem @ts-ignore;
  • você reproduziu as quatro armadilhas da Parte 9 e explicou cada comportamento observado.

Fechamento​

Quatro ideias sustentam tudo o que veio acima.

A primeira é que async não desloca cálculos para outra thread. Sobrepor esperas de rede ou disco pode reduzir a latência; processamento síncrono demorado continua bloqueando a thread em que roda. Quando o gargalo é cálculo, a ferramenta é outra thread — e o ganho tem teto antes dos núcleos, pela parte que não paraleliza.

A segunda é distinguir iniciar uma operação de observar seu resultado. Nas funções deste laboratório, a chamada inicia a espera; no Prisma, a consulta tem execução adiada. Para repetir uma operação, é preciso chamá-la de novo, não observar outra vez a mesma Promise nativa.

A terceira é que falha e demora fazem parte do caminho normal. Um código que só funciona quando tudo dá certo não está pronto — está apenas não testado.

A quarta é que uma thread só não elimina condição de corrida. Ela garante apenas que um trecho síncrono não é interrompido; entre dois await, outra chamada decide sobre o mesmo estado.

E o recorte de TypeScript, que é o que separa esta página de um tutorial de JavaScript: o tipo descreve o que se espera; a validação confere o que chegou. Na fronteira da API, as duas coisas são necessárias.


Exercícios (checkpoints)​

  1. Preveja a ordem de saída do trecho abaixo e justifique cada posição pelo modelo da Parte 1:

    console.log('A');
    setTimeout(() => console.log('B'), 0);
    Promise.resolve().then(() => {
    console.log('C');
    queueMicrotask(() => console.log('D'));
    });
    console.log('E');
  2. Explique por que const p = buscarLivro(1); inicia a espera neste laboratório e por que isso não pode ser generalizado às consultas do Prisma.

  3. Decida, para cada par, se a dependência é real ou falsa, e escreva a forma correta: (a) confirmar que um livro existe antes de contar seus empréstimos; (b) buscar três livros por identificador; (c) buscar o total de livros e o total de leitores; (d) autenticar e, com o token obtido, listar os empréstimos.

  4. Escolha o combinador para cada requisito e justifique em uma frase: (a) a tela precisa das três fichas, e sem uma delas não há o que mostrar; (b) o relatório lista o que deu certo e o que falhou; (c) três espelhos redundantes, e basta a primeira resposta boa; (d) medir quanto tempo a primeira notícia demora.

  5. Compare impor tempo limite com Promise.race e cancelar com AbortSignal. Descreva o que continua acontecendo no primeiro caso e não acontece no segundo, e indique uma situação em que essa diferença importa.

  6. Diagnostique: uma rota devolve uma lista vazia para uma listagem que deveria ter três itens. O código preenche o vetor dentro de um forEach com callback async. Explique por que o vetor está vazio e reescreva o trecho.

  7. Justifique por que catch (erro: any) é pior do que catch (erro: unknown) seguido de verificação, mesmo quando você tem certeza de que o erro é um Error. Escreva a função de verificação que resolve o caso.

  8. Explique por que converter o resultado de resposta.json() para Livro com as não garante nada em tempo de execução, e descreva o que precisa existir para que a garantia seja real.

  9. Classifique cada tarefa como limitada por CPU ou por entrada e saída e indique a ferramenta adequada: (a) buscar vinte livros por identificador; (b) gerar miniaturas de vinte capas enviadas; (c) consultar dois catálogos externos; (d) recalcular o índice de busca do acervo inteiro. Em seguida, aplique a Lei de Amdahl: se 80% do tempo de (d) pode ser paralelizado, qual é o ganho máximo com quatro núcleos, e qual com infinitos?

  10. Diagnostique: duas requisições simultâneas registram empréstimo do mesmo exemplar, embora o código verifique a disponibilidade antes de gravar. Explique por onde a segunda passou, sendo o Node de uma thread só, reescreva o trecho para fechar a janela e indique o que muda quando a API roda em duas instâncias.


Referências​

Principais​

Aprofundamento​