Introdução à programação assíncrona em JavaScript
JavaScript é uma linguagem single-thread com um loop de eventos (Event Loop) não bloqueante. Isso significa que apenas uma operação é executada por vez, mas graças à assincronicidade, não precisamos esperar a conclusão de tarefas lentas (leitura de arquivo, requisição à API, timer) e podemos continuar executando outro código. Neste artigo, abordaremos todos os conceitos-chave da programação assíncrona no ambiente Node.js, desde callbacks até a sintaxe moderna async/await.
A programação assíncrona permite criar aplicações server-side de alto desempenho que processam milhares de requisições sem bloquear a thread principal. Compreender esse paradigma é uma habilidade obrigatória para qualquer desenvolvedor JavaScript.
1. Funções Callback e o problema do Callback Hell
Historicamente, o primeiro mecanismo de assincronicidade em JavaScript foram os callbacks — funções que são chamadas após a conclusão de uma operação assíncrona.
Exemplo de um callback simples
const fs = require('fs');
// Leitura assíncrona de arquivofs.readFile('example.txt', 'utf8', (err, data) => { if (err) { console.error('Erro de leitura:', err); return; } console.log('Conteúdo do arquivo:', data);});
console.log('Esta mensagem aparecerá antes do conteúdo do arquivo');O problema surge quando precisamos executar várias operações assíncronas sequenciais. O aninhamento de callbacks cresce e o código se torna ilegível — esse fenômeno é chamado de "Callback Hell" ou "Inferno dos Callbacks".
Exemplo de Callback Hell
fs.readFile('file1.txt', (err, data1) => { if (err) throw err; fs.readFile('file2.txt', (err, data2) => { if (err) throw err; fs.readFile('file3.txt', (err, data3) => { if (err) throw err; // Processamento dos três arquivos console.log(data1 + data2 + data3); }); });});Esse código é difícil de manter, testar e depurar. A solução para esse problema veio com o surgimento das Promises.
2. Promise: livrando-se do Callback Hell
Promise (promessa) é um objeto que representa o resultado de uma operação assíncrona: execução bem-sucedida (resolve) ou erro (reject). A Promise permite encadear chamadas, tornando o código mais plano e legível.
Criação e uso de Promise
// Função que retorna uma Promiseconst readFilePromise = (filename) => { return new Promise((resolve, reject) => { fs.readFile(filename, 'utf8', (err, data) => { if (err) reject(err); else resolve(data); }); });};
// UsoreadFilePromise('file1.txt') .then(data => { console.log('Arquivo lido:', data); return readFilePromise('file2.txt'); }) .then(data => { console.log('Segundo arquivo:', data); }) .catch(err => { console.error('Erro:', err); });Métodos Promise.all e Promise.race
Para execução paralela de várias operações assíncronas, existem métodos estáticos úteis:
Promise.all()— aguarda a conclusão de todas as promises. Se pelo menos uma falhar, todo o Promise.all entra em estado rejected.Promise.race()— retorna o resultado da primeira promise concluída (sucesso ou erro).Promise.allSettled()— aguarda a conclusão de todas as promises, retornando os resultados de cada uma (sucesso ou erro).
const promise1 = readFilePromise('file1.txt');const promise2 = readFilePromise('file2.txt');const promise3 = readFilePromise('file3.txt');
Promise.all([promise1, promise2, promise3]) .then(results => { console.log('Todos os arquivos lidos:', results); }) .catch(err