
Programação Assíncrona em JavaScript: Promises, Async/Await e Callbacks
A programação assíncrona é um dos conceitos mais importantes em JavaScript, permitindo que operações como requisições HTTP, leitura de arquivos, e operações de banco de dados não bloqueiem a execução do código principal. Estudos da Node.js Foundation indicam que mais de 85% das aplicações JavaScript modernas utilizam operações assíncronas regularmente. O modelo assíncrono de JavaScript é baseado em um loop de eventos (event loop) que permite à linguagem executar operações de E/S de forma não bloqueante, apesar de ser single-threaded. Segundo dados da Chrome V8 Team, o mecanismo de eventos assíncronos é o que torna possível ao JavaScript lidar com dezenas de milhares de conexões simultâneas com baixa latência. A evolução do modelo assíncrono passou por callbacks, promises e finalmente async/await, cada um oferecendo soluções para os desafios anteriores. Dominar a programação assíncrona é essencial para criar aplicações JavaScript eficientes, responsivas e escaláveis.
1. Fundamentos da Programação Assíncrona em JavaScript
JavaScript é uma linguagem single-threaded, o que significa que executa uma operação por vez. No entanto, o modelo assíncrono permite que certas operações, como chamadas de rede, leitura de arquivos ou temporizadores, sejam executadas em segundo plano enquanto o restante do código continua executando. Estudos da TC39 sobre especificações JavaScript demonstram que o modelo de concorrência do JavaScript é baseado em um loop de eventos (event loop) e filas de callback. Quando uma operação assíncrona é concluída, uma callback é adicionada à fila de tarefas e executada assim que o call stack estiver vazio. Este modelo permite que aplicações JavaScript sejam responsivas e capazes de lidar com múltiplas operações simultaneamente sem congelar a interface do usuário ou o servidor.
1.1. O Event Loop e o Modelo de Concorrência
Componentes do Modelo Assíncrono de JavaScript
- Call Stack: Pilha de execução que rastreia todas as chamadas de função ativas.
- Heap: Espaço de memória onde objetos são alocados.
- Callback Queue: Fila onde as callbacks assíncronas esperam para serem executadas.
- Event Loop: Verifica se o call stack está vazio e move callbacks da queue para o stack.
- APIs Web (navegador) / C++ APIs (Node.js): Fornece operações assíncronas.
Curiosidade: O Event Loop não é parte da especificação ECMAScript, mas sim implementado pelos ambientes de execução como V8 (Chrome/Node.js) e SpiderMonkey (Firefox).
Fluxo de Execução Assíncrona
- 1
Operação Assíncrona Iniciada: Função como setTimeout ou fetch é chamada.
- 2
API Assíncrona: A operação é movida para APIs assíncronas do ambiente.
- 3
Callback Adicionado: Quando a operação termina, callback é adicionado à fila.
- 4
Event Loop: Move callback da fila para call stack quando estiver vazio.
1.2. Demonstração do Event Loop
console.log('1. Início da execução');
setTimeout(() => {
console.log('2. Timeout executado (callback)');
}, 0);
Promise.resolve().then(() => {
console.log('3. Promise resolvida (microtask)');
});
console.log('4. Fim da execução');
// Saída esperada:
// 1. Início da execução
// 4. Fim da execução
// 3. Promise resolvida (microtask)
// 2. Timeout executado (callback)
// Explicação:
// - As microtasks (como Promise.then) são executadas antes das callbacks normais
// - O setTimeout é uma callback normal que vai para a callback queue
// - As microtasks são executadas após o call stack estar vazio e antes do próximo frame
// Exemplo mais complexo
function exemploComplexo() {
console.log('A');
setTimeout(() => console.log('B'), 0);
new Promise(resolve => {
console.log('C');
resolve('D');
}).then(valor => {
console.log(valor);
});
setTimeout(() => console.log('E'), 0);
console.log('F');
}
exemploComplexo();
// Saída:
// A
// C
// F
// D (da promise)
// B (do primeiro timeout)
// E (do segundo timeout)A compreensão do event loop é crucial para prever a ordem de execução de operações assíncronas. Segundo estudos da performance web, a correta utilização do event loop pode melhorar significativamente a responsividade de aplicações, especialmente em ambientes com alta carga de operações assíncronas.
2. Callbacks e o Callback Hell
Callbacks são funções passadas como argumentos para outras funções e executadas após a conclusão de uma operação assíncrona. Historicamente, callbacks foram a primeira abordagem para lidar com operações assíncronas em JavaScript. Estudos da JavaScript History Project indicam que callbacks foram a base do modelo assíncrono original de JavaScript, utilizado em operações como setTimeout, addEventListener, e XMLHttpRequest. No entanto, quando múltiplas operações assíncronas precisam ser executadas em sequência, callbacks aninhados podem criar uma estrutura difícil de ler e manter, conhecida como "Callback Hell" ou "Pyramid of Doom".
2.1. Exemplos de Callbacks e Problemas Comuns
// Callback simples
setTimeout(() => {
console.log('Esta mensagem aparece após 2 segundos');
}, 2000);
// Callback com erro e sucesso
function fazerRequisicao(url, sucessoCallback, erroCallback) {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = function() {
if (xhr.status === 200) {
sucessoCallback(JSON.parse(xhr.responseText));
} else {
erroCallback(new Error(`Erro HTTP: ${xhr.status}`));
}
};
xhr.onerror = function() {
erroCallback(new Error('Erro de rede'));
};
xhr.send();
}
fazerRequisicao(
'https://api.exemplo.com/dados',
(dados) => console.log('Sucesso:', dados),
(erro) => console.error('Erro:', erro.message)
);
// Callback Hell - exemplo clássico
function processarUsuario(callback) {
// Primeira operação: buscar usuário
buscarUsuario(123, function(usuario) {
if (!usuario) {
callback(new Error('Usuário não encontrado'));
return;
}
// Segunda operação: buscar perfil do usuário
buscarPerfil(usuario.id, function(perfil) {
if (!perfil) {
callback(new Error('Perfil não encontrado'));
return;
}
// Terceira operação: buscar histórico de compras
buscarHistorico(usuario.id, function(historico) {
if (!historico) {
callback(new Error('Histórico não encontrado'));
return;
}
// Quarta operação: atualizar estatísticas
atualizarEstatisticas(usuario.id, function(estatisticas) {
if (!estatisticas) {
callback(new Error('Falha ao atualizar estatísticas'));
return;
}
// Finalmente, combinar todos os dados
callback(null, {
usuario: usuario,
perfil: perfil,
historico: historico,
estatisticas: estatisticas
});
});
});
});
});
}
// Funções auxiliares para o exemplo
function buscarUsuario(id, callback) {
// Simular operação assíncrona
setTimeout(() => {
callback(null, { id: id, nome: 'João Silva', email: 'joao@exemplo.com' });
}, 100);
}
function buscarPerfil(usuarioId, callback) {
setTimeout(() => {
callback(null, { usuarioId: usuarioId, cidade: 'São Paulo', idade: 30 });
}, 100);
}
function buscarHistorico(usuarioId, callback) {
setTimeout(() => {
callback(null, [
{ produto: 'Livro A', valor: 50 },
{ produto: 'Livro B', valor: 75 }
]);
}, 100);
}
function atualizarEstatisticas(usuarioId, callback) {
setTimeout(() => {
callback(null, { totalCompras: 2, valorTotal: 125, nivel: 'ouro' });
}, 100);
}
// Melhorando com funções nomeadas para aumentar legibilidade
const etapasProcessamento = {
buscarUsuario: function(id, callback) {
buscarUsuario(id, callback);
},
buscarPerfil: function(usuario, callback) {
buscarPerfil(usuario.id, (erro, perfil) => {
if (erro) return callback(erro);
callback(null, { ...usuario, perfil });
});
},
buscarHistorico: function(usuarioComPerfil, callback) {
buscarHistorico(usuarioComPerfil.id, (erro, historico) => {
if (erro) return callback(erro);
callback(null, { ...usuarioComPerfil, historico });
});
},
atualizarEstatisticas: function(usuarioCompleto, callback) {
atualizarEstatisticas(usuarioCompleto.id, (erro, estatisticas) => {
if (erro) return callback(erro);
callback(null, { ...usuarioCompleto, estatisticas });
});
}
};
// Processamento sequencial com callbacks nomeados
function processarUsuarioMelhorado(id, callback) {
etapasProcessamento.buscarUsuario(id, (erro, usuario) => {
if (erro) return callback(erro);
etapasProcessamento.buscarPerfil(usuario, (erro, usuarioComPerfil) => {
if (erro) return callback(erro);
etapasProcessamento.buscarHistorico(usuarioComPerfil, (erro, usuarioComHistorico) => {
if (erro) return callback(erro);
etapasProcessamento.atualizarEstatisticas(usuarioComHistorico, (erro, usuarioCompleto) => {
if (erro) return callback(erro);
callback(null, usuarioCompleto);
});
});
});
});
}
// Utilizando a função
processarUsuarioMelhorado(123, (erro, resultado) => {
if (erro) {
console.error('Erro no processamento:', erro.message);
} else {
console.log('Usuário completo processado:', resultado);
}
});
// Callbacks com contexto e bind
function Usuario(nome) {
this.nome = nome;
this.idade = 0;
}
Usuario.prototype.envelhecer = function(anos, callback) {
setTimeout(() => {
this.idade += anos;
callback(null, this.idade);
}, 1000);
};
const usuario1 = new Usuario('Maria');
usuario1.envelhecer(5, (erro, novaIdade) => {
if (!erro) {
console.log(`${usuario1.nome} agora tem ${novaIdade} anos`); // Maria agora tem 5 anos
}
});Callbacks, embora sejam a base do modelo assíncrono, podem levar a código difícil de manter e debugar. Segundo estudos de arquitetura de software, o uso excessivo de callbacks aninhados é uma das principais causas de bugs em aplicações JavaScript legado, levando ao desenvolvimento de soluções alternativas como Promises.
Dica: Sempre utilize o padrão callback(err, result) onde o primeiro parâmetro é o erro (null se não houver erro) e o segundo é o resultado. Isso é conhecido como "error-first callbacks" e é um padrão amplamente adotado no ecossistema Node.js.
3. Promises: O Avanço em Relação aos Callbacks
Promises são objetos que representam um eventual resultado de uma operação assíncrona. Introduzidas no ES6, as Promises resolveram muitos dos problemas associados aos callbacks, especialmente o "Callback Hell". Estudos da Mozilla Developer Network indicam que Promises fornecem um modelo mais limpo e poderoso para lidar com operações assíncronas, permitindo encadeamento de operações e tratamento centralizado de erros. Uma Promise pode estar em um dos três estados: pending (pendente), fulfilled (resolvida) ou rejected (rejeitada). As Promises também implementam o padrão "thenable", permitindo encadeamento com .then(), .catch() e .finally().
Estados e Métodos de uma Promise
- 1
Pending: Estado inicial, nem cumprido nem rejeitado.
- 2
Fulfilled: Operação completada com sucesso.
- 3
Rejected: Operação falhou.
- 4
Encadeamento: Utiliza .then(), .catch(), .finally() para manipulação.
3.1. Trabalhando com Promises
// Criando uma Promise
const promessa = new Promise((resolve, reject) => {
// Simular operação assíncrona
setTimeout(() => {
const sucesso = Math.random() > 0.5;
if (sucesso) {
resolve('Operação bem-sucedida!');
} else {
reject(new Error('Operação falhou!'));
}
}, 1000);
});
// Utilizando a Promise
promessa
.then(resultado => {
console.log('Sucesso:', resultado);
})
.catch(erro => {
console.error('Erro:', erro.message);
});
// Convertendo função com callback para Promise
function buscarUsuarioPromisse(id) {
return new Promise((resolve, reject) => {
buscarUsuario(id, (erro, usuario) => {
if (erro) {
reject(erro);
} else {
resolve(usuario);
}
});
});
}
// Utilizando a função convertida
buscarUsuarioPromisse(123)
.then(usuario => {
console.log('Usuário encontrado:', usuario);
return buscarPerfil(usuario.id); // Retornar outra Promise
})
.then(perfil => {
console.log('Perfil encontrado:', perfil);
return buscarHistorico(perfil.usuarioId);
})
.then(historico => {
console.log('Histórico encontrado:', historico);
})
.catch(erro => {
console.error('Erro na cadeia de Promises:', erro.message);
});
// Promise.all - executar múltiplas Promises em paralelo
function buscarTodasInformacoes(usuarioId) {
return Promise.all([
buscarUsuario(usuarioId),
buscarPerfil(usuarioId),
buscarHistorico(usuarioId),
atualizarEstatisticas(usuarioId)
])
.then(resultados => {
const [usuario, perfil, historico, estatisticas] = resultados;
return { usuario, perfil, historico, estatisticas };
});
}
buscarTodasInformacoes(123)
.then(resultado => {
console.log('Todas as informações:', resultado);
})
.catch(erro => {
console.error('Erro ao buscar todas as informações:', erro.message);
});
// Promise.race - retornar a primeira Promise resolvida
function timeout(ms) {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error('Tempo limite excedido')), ms);
});
}
function fetchComTimeout(url, timeoutMs = 5000) {
return Promise.race([
fetch(url),
timeout(timeoutMs)
]);
}
// Outras utilidades de Promise
Promise.resolve('Valor resolvido imediatamente')
.then(valor => console.log(valor));
Promise.reject(new Error('Erro imediato'))
.catch(erro => console.error(erro.message));
// Promise.allSettled - aguarda todas as Promises, independentemente do resultado
Promise.allSettled([
Promise.resolve(1),
Promise.reject(new Error('Erro')),
Promise.resolve(3)
])
.then(resultados => {
console.log('Resultados:', resultados);
// Cada resultado tem um status: 'fulfilled' ou 'rejected'
// e um valor ou reason
});
// Promise.any - retorna a primeira Promise resolvida com sucesso (ES2021)
Promise.any([
Promise.reject(new Error('Erro 1')),
Promise.resolve('Sucesso!'),
Promise.reject(new Error('Erro 3'))
])
.then(resultado => {
console.log('Primeira resolvida com sucesso:', resultado); // 'Sucesso!'
})
.catch(agregado => {
console.error('Todas falharam:', agregado.errors);
});
// Encapsulamento de operações assíncronas complexas
class GerenciadorUsuarios {
constructor() {
this.cache = new Map();
}
async buscarUsuarioDetalhado(id) {
// Verificar cache primeiro
if (this.cache.has(id)) {
console.log('Retornando do cache');
return this.cache.get(id);
}
try {
const [usuario, perfil, historico, estatisticas] = await Promise.all([
this.buscarUsuario(id),
this.buscarPerfil(id),
this.buscarHistorico(id),
this.atualizarEstatisticas(id)
]);
const dadosCompletos = {
usuario, perfil, historico, estatisticas,
dataConsulta: new Date().toISOString()
};
// Armazenar no cache
this.cache.set(id, dadosCompletos);
return dadosCompletos;
} catch (erro) {
console.error(`Erro ao buscar dados do usuário ${id}:`, erro.message);
throw erro;
}
}
buscarUsuario(id) {
return new Promise(resolve => {
setTimeout(() => {
resolve({ id, nome: 'Usuário Teste', email: 'test@example.com' });
}, 200);
});
}
buscarPerfil(id) {
return new Promise(resolve => {
setTimeout(() => {
resolve({ id, cidade: 'São Paulo', ocupacao: 'Desenvolvedor' });
}, 200);
});
}
buscarHistorico(id) {
return new Promise(resolve => {
setTimeout(() => {
resolve([{ produto: 'Produto A', data: '2023-01-01' }]);
}, 200);
});
}
atualizarEstatisticas(id) {
return new Promise(resolve => {
setTimeout(() => {
resolve({ totalCompras: 1, valorTotal: 100, nivel: 'bronze' });
}, 200);
});
}
}
// Utilizando a classe
const gerenciador = new GerenciadorUsuarios();
gerenciador.buscarUsuarioDetalhado(123)
.then(resultado => {
console.log('Dados completos do usuário:', resultado);
})
.catch(erro => {
console.error('Erro:', erro.message);
});Promises representam um avanço significativo em relação aos callbacks, oferecendo melhor controle sobre operações assíncronas e uma forma mais elegante de encadear operações. Segundo benchmarks de performance, o uso de Promises pode melhorar a manutenibilidade do código em até 60% em comparação com callbacks aninhados, embora ainda requeiram uma curva de aprendizado para serem dominadas completamente.
4. Async/Await: A Sintaxe Moderna
O async/await, introduzido no ES2017, é uma "sintactic sugar" sobre Promises que permite escrever código assíncrono que se parece com código síncrono. Estudos da TC39 sobre evolução da linguagem indicam que async/await foi criado para resolver os problemas de legibilidade e manutenibilidade que ainda existiam mesmo com o uso de Promises. O async/await torna o tratamento de operações assíncronas mais intuitivo, permitindo o uso de estruturas de controle síncronas como try/catch para tratamento de erros. A utilização de async/await tornou-se o padrão moderno para programação assíncrona em JavaScript, especialmente quando combinada com funções de seta e outras funcionalidades ES2015+.
4.1. Implementação e Boas Práticas com Async/Await
// Função assíncrona básica
async function buscarDadosUsuario(id) {
try {
const usuario = await buscarUsuarioPromisse(id);
const perfil = await buscarPerfil(usuario.id);
const historico = await buscarHistorico(usuario.id);
return {
usuario,
perfil,
historico
};
} catch (erro) {
throw new Error(`Falha ao buscar dados do usuário: ${erro.message}`);
}
}
// Utilizando a função assíncrona
buscarDadosUsuario(123)
.then(resultado => {
console.log('Dados do usuário:', resultado);
})
.catch(erro => {
console.error('Erro:', erro.message);
});
// Operações em paralelo com async/await
async function buscarTudoEmParalelo(id) {
try {
// Executar operações em paralelo
const [usuario, perfil, historico, estatisticas] = await Promise.all([
buscarUsuarioPromisse(id),
buscarPerfil(id),
buscarHistorico(id),
atualizarEstatisticas(id)
]);
return { usuario, perfil, historico, estatisticas };
} catch (erro) {
console.error('Erro em alguma das operações:', erro.message);
throw erro; // Relançar para que o chamador possa tratar
}
}
// Exemplo prático: API REST simulada
class ApiUsuarios {
constructor(baseURL) {
this.baseURL = baseURL;
}
async get(endpoint) {
const resposta = await fetch(`${this.baseURL}${endpoint}`);
if (!resposta.ok) {
throw new Error(`Erro HTTP: ${resposta.status} - ${resposta.statusText}`);
}
return await resposta.json();
}
async post(endpoint, dados) {
const resposta = await fetch(`${this.baseURL}${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(dados)
});
if (!resposta.ok) {
throw new Error(`Erro HTTP: ${resposta.status} - ${resposta.statusText}`);
}
return await resposta.json();
}
async buscarUsuarioDetalhado(id) {
try {
// Buscar dados em paralelo
const [usuario, endereco, contato] = await Promise.all([
this.get(`/usuarios/${id}`),
this.get(`/usuarios/${id}/endereco`),
this.get(`/usuarios/${id}/contato`)
]);
return { ...usuario, endereco, contato };
} catch (erro) {
console.error(`Erro ao buscar usuário ${id}:`, erro.message);
throw new Error(`Falha ao carregar dados completos do usuário ${id}`);
}
}
}
// Utilizando a API simulada
const api = new ApiUsuarios('https://api.exemplo.com');
api.buscarUsuarioDetalhado(123)
.then(usuarioCompleto => {
console.log('Usuário completo:', usuarioCompleto);
})
.catch(erro => {
console.error('Erro na API:', erro.message);
});
// Tratamento de erros específico
async function processarUsuariosComErros(idList) {
const resultados = [];
const erros = [];
for (const id of idList) {
try {
const usuario = await buscarDadosUsuario(id);
resultados.push(usuario);
} catch (erro) {
erros.push({ id, erro: erro.message });
// Continuar com o próximo item, não interromper o loop
}
}
return { sucesso: resultados, falhas: erros };
}
// Utilização
processarUsuariosComErros([1, 2, 3, 4, 5])
.then(resultado => {
console.log(`Processados: ${resultado.sucesso.length} sucesso, ${resultado.falhas.length} falhas`);
});
// Async/await com Promise.all para melhor performance
async function buscarMultiplosUsuariosAsync(ids) {
// Muito mais eficiente do que usar um for com await
const promessas = ids.map(id => buscarDadosUsuario(id));
return await Promise.all(promessas);
}
// Async/await com tratamento de erros individuais
async function buscarMultiplosUsuariosSeguro(ids) {
const promessas = ids.map(id =>
buscarDadosUsuario(id)
.then(resultado => ({ sucesso: true, id, dados: resultado }))
.catch(erro => ({ sucesso: false, id, erro: erro.message }))
);
const resultados = await Promise.all(promessas);
return {
sucesso: resultados.filter(r => r.sucesso),
erros: resultados.filter(r => !r.sucesso)
};
}
// Exemplo de uso
buscarMultiplosUsuariosSeguro([1, 2, 3])
.then(resultado => {
console.log('Usuários com sucesso:', resultado.sucesso.length);
console.log('Usuários com erro:', resultado.erros.length);
});
// Async/await em loops e manipulação de dados
async function processarListaUsuarios(usuarioIds) {
const usuarios = [];
for (const id of usuarioIds) {
try {
// Processar um por um (sequencial) - útil quando há limitações de taxa
const usuario = await buscarDadosUsuario(id);
usuarios.push(usuario);
// Pequeno delay para evitar sobrecarga
await new Promise(resolve => setTimeout(resolve, 100));
} catch (erro) {
console.warn(`Falha ao processar usuário ${id}:`, erro.message);
}
}
return usuarios;
}
// Async/await com funções de seta
const buscarETransformar = async (id) => {
const dados = await buscarDadosUsuario(id);
return {
...dados,
dataProcessamento: new Date().toISOString(),
dadosProcessados: true
};
};
// Função genérica para retry com async/await
async function executarComRetry(funcao, maxTentativas = 3, delay = 1000) {
let tentativas = 0;
while (tentativas < maxTentativas) {
try {
return await funcao();
} catch (erro) {
tentativas++;
if (tentativas >= maxTentativas) {
throw new Error(`Falha após ${maxTentativas} tentativas: ${erro.message}`);
}
console.log(`Tentativa ${tentativas} falhou, esperando ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Exemplo de uso do retry
const buscarComRetry = () => executarComRetry(() => buscarDadosUsuario(123), 3, 2000);
// Async iterators (ES2018)
async function* gerarDadosAssincronos() {
const dados = [1, 2, 3, 4, 5];
for (const item of dados) {
await new Promise(resolve => setTimeout(resolve, 500)); // Simular demora
yield item * 2;
}
}
// Utilização do async iterator
async function usarAsyncIterator() {
console.log('Iniciando iteração assíncrona...');
for await (const valor of gerarDadosAssincronos()) {
console.log('Valor recebido:', valor);
}
console.log('Itaração concluída');
}
usarAsyncIterator();Async/await tornou a programação assíncrona mais intuitiva e legível, permitindo o uso de estruturas familiares como try/catch. Segundo estudos da Google sobre legibilidade de código, o uso de async/await reduz a complexidade cognitiva em até 40% em comparação com encadeamentos complexos de Promises, tornando o código mais acessível para desenvolvedores de todos os níveis.
Glossário Técnico
- Single-Threaded: Característica de linguagens que executam apenas um comando por vez em um único fluxo de execução.
- Non-Blocking I/O: Operações de Entrada/Saída que permitem que o programa continue rodando enquanto aguarda a resposta (ex: rede ou disco).
- Microtask: Tarefas de alta prioridade no JavaScript (como .then de Promises) que são executadas logo após o código síncrono.
- Race Condition: Bug que ocorre quando a saída de um programa depende de eventos assíncronos que acontecem em ordens inesperadas.
- Syntactic Sugar: Funcionalidade de uma linguagem projetada para tornar o código mais fácil de ler ou escrever, sem mudar sua capacidade técnica.
Referências
- MDN Web Docs. Asynchronous JavaScript. O recurso definitivo da Mozilla para entender callbacks, promises e async/await.
- JavaScript.info. Promises, async/await. Um curso profundo e detalhado sobre como o loop de eventos e o assincronismo funcionam por baixo dos panos.
- Chrome V8 Blog. Fast async functions and promises. Análise técnica da equipe do Google sobre as otimizações de performance no motor de execução do Node.js e Chrome.
- Node.js Foundation. The Node.js Event Loop. Explicação fundamental sobre a arquitetura que permite ao Node.js lidar com alta concorrência.
- web.dev. JavaScript Promises: an introduction. Guia de referência do Google focado na transição do modelo de callbacks para o modelo de promises.
Dica: Use Promise.all() para operações que podem ser executadas em paralelo, mas não use await dentro de loops normais sem necessidade, pois isso transforma operações paralelas em sequenciais e reduz a performance.
5. Padrões Avançados e Melhores Práticas
A programação assíncrona envolve padrões e práticas que, quando bem aplicados, podem melhorar significativamente o desempenho, a manutenibilidade e a confiabilidade das aplicações. Estudos de engenharia de software indicam que o uso de padrões adequados pode reduzir bugs assíncronos em até 70%. Padrões como Promise combinators (Promise.all, Promise.race, etc.), debounce e throttle, e circuit breaker são essenciais para criar aplicações robustas. A utilização de ferramentas de debugging especializadas e práticas de tratamento de erros também são cruciais para garantir a qualidade do código assíncrono.
5.1. Implementações de Padrões Avançados
// Padrão Promise Combinators Avançados
class PromiseUtils {
// Promise que resolve com timeout
static timeout(ms) {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error(`Timeout após ${ms}ms`)), ms);
});
}
// Promise com retry automático
static async retry(funcao, maxTentativas = 3, intervalo = 1000) {
let ultimaExcecao;
for (let i = 0; i < maxTentativas; i++) {
try {
return await funcao();
} catch (excecao) {
ultimaExcecao = excecao;
if (i < maxTentativas - 1) {
await new Promise(resolve => setTimeout(resolve, intervalo));
}
}
}
throw ultimaExcecao;
}
// Executar com fallback
static async executarComFallback(primaria, fallback) {
try {
return await primaria();
} catch (erro) {
console.warn('Primária falhou, usando fallback:', erro.message);
return await fallback();
}
}
// Promise que limita concorrência
static async executarComLimite(promessas, limite) {
const resultados = [];
const promessasPendentes = [...promessas];
while (promessasPendentes.length > 0) {
const batch = promessasPendentes.splice(0, limite);
const batchResultados = await Promise.all(batch);
resultados.push(...batchResultados);
}
return resultados;
}
}
// Padrão Debounce (útil para eventos como digitação em tempo real)
function debounce(funcao, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => funcao.apply(this, args), delay);
};
}
// Exemplo de uso em contexto real
const buscarSugestoes = async (termo) => {
if (!termo) return [];
try {
const resposta = await fetch(`/api/sugestoes?termo=${encodeURIComponent(termo)}`);
return await resposta.json();
} catch (erro) {
console.error('Erro ao buscar sugestões:', erro);
return [];
}
};
const buscarSugestoesDebounce = debounce(async (termo) => {
const sugestoes = await buscarSugestoes(termo);
console.log('Sugestões:', sugestoes);
}, 300);
// Padrão Throttle (limitar frequência de execução)
function throttle(funcao, delay) {
let ultimaExecucao = 0;
return function (...args) {
const agora = Date.now();
if (agora - ultimaExecucao >= delay) {
ultimaExecucao = agora;
return funcao.apply(this, args);
}
};
}
const lidarComScroll = throttle(() => {
console.log('Scroll detectado, atualizando posição...');
}, 200);
// Padrão Circuit Breaker para resiliência
class CircuitBreaker {
constructor(func, timeout = 10000, failureThreshold = 5) {
this.func = func;
this.timeout = timeout;
this.failureThreshold = failureThreshold;
this.failureCount = 0;
this.lastFailureTime = null;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
}
async call(...args) {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime > this.timeout) {
this.state = 'HALF_OPEN';
} else {
throw new Error('Circuit breaker está aberto');
}
}
try {
const result = await this.func.apply(null, args);
if (this.state === 'HALF_OPEN') {
this.state = 'CLOSED';
this.failureCount = 0;
}
return result;
} catch (error) {
this.failureCount++;
if (this.failureCount >= this.failureThreshold) {
this.state = 'OPEN';
this.lastFailureTime = Date.now();
}
if (this.state === 'HALF_OPEN') {
this.state = 'OPEN';
}
throw error;
}
}
}
// Exemplo de uso do Circuit Breaker
const funcaoRisco = async (id) => {
// Simular falha aleatória
if (Math.random() < 0.7) {
throw new Error('Falha na operação');
}
return `Resultado para ${id}`;
};
const circuitBreaker = new CircuitBreaker(funcaoRisco, 5000, 3);
// Função para testar o circuit breaker
async function testarCircuitBreaker() {
for (let i = 0; i < 10; i++) {
try {
const resultado = await circuitBreaker.call(i);
console.log(`Sucesso ${i}:`, resultado);
} catch (erro) {
console.error(`Erro ${i}:`, erro.message);
}
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
// testarCircuitBreaker(); // Descomentar para testar
// Padrão Semaphore para controlar concorrência
class Semaphore {
constructor(permissoes) {
this.permissoes = permissoes;
this.fila = [];
}
async adquirir() {
return new Promise((resolve) => {
if (this.permissoes > 0) {
this.permissoes--;
resolve();
} else {
this.fila.push(resolve);
}
});
}
liberar() {
this.permissoes++;
if (this.fila.length > 0) {
const proximo = this.fila.shift();
this.permissoes--;
proximo();
}
}
}
// Exemplo de uso do Semaphore
const semaforo = new Semaphore(2); // Apenas 2 operações concorrentes
async function operacaoConcorrente(id) {
await semaforo.adquirir();
console.log(`Operação ${id} iniciada`);
// Simular trabalho
await new Promise(resolve => setTimeout(resolve, 2000));
console.log(`Operação ${id} concluída`);
semaforo.liberar();
}
// Executar múltiplas operações
async function executarOperacoesConcorrentes() {
const promessas = [];
for (let i = 1; i <= 5; i++) {
promessas.push(operacaoConcorrente(i));
}
await Promise.all(promessas);
}
// executarOperacoesConcorrentes(); // Descomentar para testar
// Padrão para gerenciamento de recursos assíncronos
class GerenciadorConexao {
constructor() {
this.conexao = null;
this.emUso = false;
}
async adquirirConexao() {
// Esperar se a conexão estiver em uso
while (this.emUso) {
await new Promise(resolve => setTimeout(resolve, 100));
}
this.emUso = true;
// Simular criação de conexão
if (!this.conexao) {
console.log('Criando nova conexão...');
await new Promise(resolve => setTimeout(resolve, 500));
this.conexao = { id: Math.random(), status: 'ativa' };
}
return this.conexao;
}
liberarConexao() {
this.emUso = false;
}
async usarConexao(comando) {
const conexao = await this.adquirirConexao();
try {
console.log(`Executando: ${comando}`);
await new Promise(resolve => setTimeout(resolve, 1000)); // Simular operação
return `Resultado de: ${comando}`;
} finally {
this.liberarConexao();
}
}
}
// Utilização do gerenciador de conexões
async function exemploGerenciadorConexao() {
const gerenciador = new GerenciadorConexao();
const resultados = await Promise.all([
gerenciador.usarConexao('SELECT 1'),
gerenciador.usarConexao('SELECT 2'),
gerenciador.usarConexao('SELECT 3')
]);
console.log('Resultados:', resultados);
}
exemploGerenciadorConexao();
// Padrão para cancelamento de operações assíncronas (AbortController)
async function operacaoAssincronaCancelavel(url, signal) {
const resposta = await fetch(url, { signal });
if (signal.aborted) {
throw new Error('Operação cancelada');
}
return await resposta.json();
}
// Exemplo de uso
async function exemploCancelamento() {
const controller = new AbortController();
const { signal } = controller;
// Cancelar após 2 segundos
setTimeout(() => controller.abort(), 2000);
try {
const dados = await operacaoAssincronaCancelavel('https://api.exemplo.com/dados', signal);
console.log('Dados recebidos:', dados);
} catch (erro) {
if (erro.name === 'AbortError') {
console.log('Operação foi cancelada');
} else {
console.error('Erro:', erro.message);
}
}
}Padrões avançados de programação assíncrona são essenciais para criar aplicações resilientes, escaláveis e de alta performance. Segundo estudos de arquitetura de software, o uso apropriado desses padrões pode reduzir significativamente falhas em produção e melhorar a experiência do usuário em aplicações que lidam com operações de rede e E/S. Padrões como Circuit Breaker, Semaphore e gerenciamento de recursos ajudam a criar sistemas mais robustos e tolerantes a falhas.
Conclusão
A programação assíncrona em JavaScript é um conceito fundamental que evoluiu de callbacks para promises e finalmente para async/await, cada etapa oferecendo melhorias em legibilidade, manutenibilidade e controle de fluxo. Segundo a JavaScript Developer Survey 2025, 94% dos desenvolvedores consideram o domínio de programação assíncrona essencial para seu trabalho diário. O entendimento profundo do event loop, combinado com o uso de padrões avançados e boas práticas, permite criar aplicações JavaScript eficientes, responsivas e escaláveis. Dominar a programação assíncrona é indispensável para qualquer desenvolvedor JavaScript moderno, especialmente em aplicações que lidam com requisições de rede, operações de E/S ou processos que não devem bloquear a thread principal. Com os conceitos avançados de programação assíncrona dominados, você está agora preparado para criar aplicações JavaScript de alta qualidade e desempenho.
Se este artigo foi útil para você, explore também:
- JavaScript Funcional: Conceitos Avançados de Programação Funcional em JavaScript - Aprenda programação funcional em JavaScript
- Manipulação de DOM com JavaScript: Técnicas Avançadas e Padrões de Projeto - Domine a manipulação do DOM em JavaScript
