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.
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
Promiserepresenta, 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.alle uma thread adicional. - Diagnosticar uma condição de corrida entre dois
awaitem código de uma thread só e corrigi-la sem recorrer a travas. - Escolher entre
all,allSettled,raceeanydiante 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
catche dados vindos de fora, sem recorrer aany. - 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:
| Sintoma | O que está por trás |
|---|---|
| A listagem demora 900 ms onde 300 bastariam | Três await em sequência para chamadas independentes |
O try/catch não capturou um erro que aconteceu | Faltou o await: o bloco terminou antes de o erro chegar |
| O vetor está vazio depois do laço que o preencheu | forEach com callback async: o laço não espera |
catch (erro: any) em todo lugar | O 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ência | Trabalho limitado por CPU: assincronismo sobrepõe espera, não cálculo |
| O mesmo exemplar foi emprestado duas vezes | Checagem 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.
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:
| Termo | O que descreve | Pergunta que responde |
|---|---|---|
| Concorrência | várias tarefas em andamento no mesmo intervalo, sem exigir que executem no mesmo instante | como o programa está organizado |
| Paralelismo | duas ou mais tarefas executando no mesmo instante, em núcleos diferentes | como o trabalho é executado |
| Assincronismo | o modelo de programação que inicia a operação, libera a thread e retoma quando o resultado chega | como 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.
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.
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
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');
}
}
try sem await é uma armadilha silenciosaSe 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:
- Sequencial (evitar aqui)
- Concorrente (preferir)
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.
const livros = await Promise.all([buscarLivro(1), buscarLivro(2), buscarLivro(3)]);
As três chamadas foram disparadas na criação do vetor, antes de qualquer espera. Neste simulador, o tempo fica próximo ao do mais lento: 300 ms, mais o custo de execução. Em um serviço real, filas e limites podem aumentá-lo.
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));
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.
| Tarefa | Onde o tempo vai | Ferramenta |
|---|---|---|
| Consultar o acervo no PostgreSQL | espera | await, Promise.all |
| Chamar o catálogo externo de capas | espera | Promise.all, allSettled |
| Ler um arquivo grande de importação | espera | iteração assíncrona (Parte 8) |
| Gerar miniaturas das capas enviadas | cálculo | outra thread |
| Montar o índice de busca do acervo | cálculo | outra thread |
| Produzir o relatório mensal em PDF | cálculo | outra 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
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.
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 do tempo total pode ser paralelizada e o resto é inerentemente sequencial, o ganho máximo com processadores é
| Fração paralelizável | 2 núcleos | 4 núcleos | 8 núcleos | Infinitos |
|---|---|---|---|---|
| 50% | 1,33 vez | 1,60 vez | 1,78 vez | 2 vezes |
| 90% | 1,82 vez | 3,08 vezes | 4,71 vezes | 10 vezes |
| 99% | 1,98 vez | 3,88 vezes | 7,48 vezes | 100 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]
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 comum | O 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:
- O tempo vai em espera ou em cálculo? Meça antes de mudar o código.
- As tarefas dependem umas das outras? É a pergunta da Parte 4.
- Elas decidem sobre o mesmo estado? Se sim, procure a janela entre a decisão e a escrita.
- 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.
| Fonte | Tempo | Desfecho |
|---|---|---|
| catálogo do parceiro | 100 ms | rejeita |
| acervo local | 250 ms | responde |
| catálogo nacional | 400 ms | responde |
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:
| Combinador | Use quando | Liquida |
|---|---|---|
Promise.all | precisa de todas; qualquer falha invalida o conjunto | na primeira rejeição, ou quando a última realizar |
Promise.allSettled | precisa do relatório completo, sucesso e falha | quando todas liquidarem; rejeições de entrada viram registros |
Promise.race | precisa da primeira notícia, seja qual for | na primeira que liquidar, inclusive rejeitando |
Promise.any | precisa da primeira resposta boa | na 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.
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
}
É 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.
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 observado | Armadilha provável |
|---|---|
| Vetor vazio logo depois do laço que o preenche | 1 — forEach com async |
O catch não capturou um erro que aconteceu | 2 — falta de await no try |
| O processo terminou por uma rejeição não tratada | 3 — promise solta |
O cálculo bloqueia mesmo quando a chamada usa await | 4 — await no que não é promise |
Parte 10 — Onde isso já apareceu no semestre
Estas conexões situam o aprofundamento no percurso da disciplina:
| Onde | O que era, de fato |
|---|---|
LivrosService.listar() da Aula 6 | Retorna dados síncronos do acervo em memória; usar NestJS não obriga a usar async |
| Toda chamada ao Prisma na Aula 7 | PrismaPromise: consultas com execução adiada, consumidas por await ou compostas em uma transação |
| As transações da Aula 9 | Garantem 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 11 | Um componente async: o React aguarda a promise antes de renderizar |
O params assíncrono do Next.js 16 | params deve ser aguardado antes do acesso aos campos; tipos e runtime podem diagnosticar o acesso incorreto |
| O hash de senha da Aula 10 | Cá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 4 | Conceitos 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 download | Pasta de destino |
|---|---|
| package.json | raiz |
| tsconfig.json | raiz |
| tsconfig.praticar.json | raiz |
| README.md | raiz |
| src/01-ordem-execucao.ts | src |
| src/02-callback-para-promise.ts | src |
| src/03-promises.ts | src |
| src/04-async-await.ts | src |
| src/05-sequencial-concorrente.ts | src |
| src/06-combinadores.ts | src |
| src/07-tempo-limite-repeticao.ts | src |
| src/08-tipagem.ts | src |
| src/09-iteracao-assincrona.ts | src |
| src/10-armadilhas.ts | src |
| src/11-concorrencia-paralelismo.ts | src |
| src/12-corrida-logica.ts | src |
| src/assinatura.worker.ts | src |
| src/biblioteca.ts | src |
| praticar/01-ordem-execucao.ts | praticar |
| praticar/02-ficha-do-livro.ts | praticar |
| praticar/03-sequencial-concorrente.ts | praticar |
| praticar/04-combinadores.ts | praticar |
| praticar/05-tempo-limite.ts | praticar |
| praticar/06-tipagem.ts | praticar |
| praticar/LEIA-ME.md | praticar |
Na pasta extra-a-assincronismo, execute:
npm install
npm run check
npx tsx src/01-ordem-execucao.ts
| Comando | O que faz |
|---|---|
npm run check | verifica os tipos dos exemplos de src |
npm run check:praticar | verifica também os exercícios de praticar |
npx tsx src/NN-arquivo.ts | executa um exemplo; substitua pelo nome real |
npx tsx praticar/NN-arquivo.ts | executa 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.
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/awaitdo 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
awaitque 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
AbortSignaldeixa de imprimir a linha que a versão comraceainda imprimia; -
npm run check:praticarpassa no laboratório 6 sem nenhumanye 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)
-
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'); -
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. -
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.
-
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.
-
Compare impor tempo limite com
Promise.racee cancelar comAbortSignal. 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. -
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
forEachcom callbackasync. Explique por que o vetor está vazio e reescreva o trecho. -
Justifique por que
catch (erro: any)é pior do quecatch (erro: unknown)seguido de verificação, mesmo quando você tem certeza de que o erro é umError. Escreva a função de verificação que resolve o caso. -
Explique por que converter o resultado de
resposta.json()paraLivrocomasnão garante nada em tempo de execução, e descreva o que precisa existir para que a garantia seja real. -
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?
-
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
- MDN — Usando Promises — o guia introdutório, em português
- MDN — Promise — referência completa, incluindo os quatro combinadores
- Node.js — The Node.js Event Loop — as fases do laço e o lugar das microtasks
- MDN — Modelo de concorrência e event loop — pilha, fila e a execução até o fim
- TypeScript Handbook — Utility Types —
Awaited,ReturnTypee os demais - Prisma — PrismaPromise — execução adiada das consultas
- Prisma — Transações — atomicidade e diferenças em relação a
Promise.all - MDN — AbortController — cancelamento cooperativo, o mesmo que
fetchaceita - Node.js — Worker threads — threads de verdade no Node, com memória isolada e troca de mensagens
- Node.js — Don't block the event loop — por que trabalho de CPU na thread principal derruba a vazão do servidor
Aprofundamento
- Jake Archibald — Tasks, microtasks, queues and schedules — a explicação de referência sobre a diferença entre as duas filas
- MDN — for await...of — iteração sobre fontes assíncronas
- MDN — AsyncGenerator — o protocolo por trás do
async function* - TypeScript 4.4 — useUnknownInCatchVariables — por que o
catchpassou a receberunknown - Concurrency is not Parallelism, palestra de Rob Pike (2012) — vídeo e slides, no blog do Go: a distinção citada na Parte 1
- Gene Amdahl — Validity of the single processor approach to achieving large scale computing capabilities (AFIPS, 1967) — o artigo original do limite de ganho
- Dart — Asynchronous programming — o mesmo modelo no Módulo 4, com
Futureno lugar dePromise