Pular para o conteúdo principal

Programação Assíncrona em JavaScript: Promises, Async/Await e Callbacks

Publicado em 28 de dezembro de 202526 min de leitura
Imagem de tecnologia relacionada ao artigo programacao-assincrona-javascript-promises-async-await

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. 1

    Operação Assíncrona Iniciada: Função como setTimeout ou fetch é chamada.

  2. 2

    API Assíncrona: A operação é movida para APIs assíncronas do ambiente.

  3. 3

    Callback Adicionado: Quando a operação termina, callback é adicionado à fila.

  4. 4

    Event Loop: Move callback da fila para call stack quando estiver vazio.

1.2. Demonstração do Event Loop

javascript
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

javascript
// 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. 1

    Pending: Estado inicial, nem cumprido nem rejeitado.

  2. 2

    Fulfilled: Operação completada com sucesso.

  3. 3

    Rejected: Operação falhou.

  4. 4

    Encadeamento: Utiliza .then(), .catch(), .finally() para manipulação.

3.1. Trabalhando com Promises

javascript
// 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

javascript
// 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

  1. MDN Web Docs. Asynchronous JavaScript. O recurso definitivo da Mozilla para entender callbacks, promises e async/await.
  2. JavaScript.info. Promises, async/await. Um curso profundo e detalhado sobre como o loop de eventos e o assincronismo funcionam por baixo dos panos.
  3. 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.
  4. Node.js Foundation. The Node.js Event Loop. Explicação fundamental sobre a arquitetura que permite ao Node.js lidar com alta concorrência.
  5. 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

javascript
// 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:

Imagem de tecnologia relacionada ao artigo programacao-assincrona-javascript-promises-async-await