Programação Assíncrona em JavaScript: de callbacks a async/await
JavaScript é uma linguagem single-thread, mas seu modelo de execução permite processar eficientemente milhares de operações simultâneas sem bloquear a thread principal. Isso é possível graças à programação assíncrona. No Node.js, onde operações de entrada/saída (leitura de arquivos, consultas a banco de dados, chamadas de rede) ocupam 90% do tempo, entender a assincronicidade torna-se uma habilidade crítica. Neste guia, vamos abordar três estágios evolutivos do trabalho com assincronicidade: callbacks, promises e async/await, além de explorar os bastidores do Event Loop.
1. Callbacks: o fundamento da assincronicidade
Um callback é uma função que é passada para outra função como argumento e é chamada após a conclusão de uma operação assíncrona. Este é o padrão mais antigo e básico em JavaScript. No Node.js, muitos módulos nativos (fs, http) usam callbacks.
Exemplo de leitura de arquivo com callback:
const fs = require('fs');
console.log('Iniciando leitura do arquivo...');
fs.readFile('example.txt', 'utf8', (err, data) => { if (err) { console.error('Erro:', err); return; } console.log('Conteúdo do arquivo:', data);});
console.log('O código continua executando...');A saída no console mostrará que "O código continua executando..." aparecerá antes do conteúdo do arquivo. Este é o comportamento não bloqueante: a thread principal não espera a leitura terminar, mas continua a execução.
Problema dos callbacks: Callback Hell
Com operações assíncronas aninhadas, o código se transforma em uma "pirâmide infernal":
fs.readFile('user.json', (err, userData) => { if (err) throw err; const user = JSON.parse(userData); db.query(`SELECT * FROM posts WHERE user_id=${user.id}`, (err, posts) => { if (err) throw err; fs.writeFile('user_posts.json', JSON.stringify(posts), (err) => { if (err) throw err; console.log('Pronto!'); }); });});Esse código é difícil de ler, depurar e manter. A solução são as promises.
2. Promises: encadeamento e tratamento de erros
Uma Promise é um objeto que representa o resultado futuro de uma operação assíncrona. Ela pode estar em um de três estados: pending (pendente), fulfilled (concluída com sucesso) ou rejected (rejeitada com erro).
Criação de uma promise simples:
const readFilePromise = (filename) => { return new Promise((resolve, reject) => { fs.readFile(filename, 'utf8', (err, data) => { if (err) { reject(err); } else { resolve(data); } }); });};
readFilePromise('example.txt') .then(data => console.log('Dados:', data)) .catch(err => console.error('Erro:', err));Encadeamento de Promises (Chaining)
A principal vantagem é a capacidade de encadear operações sequenciais sem aninhamento:
readFilePromise('user.json') .then(userData => { const user = JSON.parse(userData); return dbQueryPromise(`SELECT * FROM posts WHERE user_id=${user.id}`); }) .then(posts => { return writeFilePromise('user_posts.json', JSON.stringify(posts)); }) .then(() => console.log('Pronto!')) .catch(err => console.error('Erro na cadeia:', err));Execução paralela: Promise.all
Quando é necessário executar várias operações independentes simultaneamente:
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 => console.error('Uma das promises falhou:', err));Promise.all é concluída com sucesso apenas se todas as promises forem bem-sucedidas. Se pelo menos uma falhar, o catch será acionado.