A sintaxe async/await, introduzida no ES7, revolucionou a programação assíncrona em JavaScript. Ela nos permite escrever código assíncrono com uma aparência síncrona, melhorando drasticamente a legibilidade e a manutenibilidade do código.
No entanto, apesar das suas vantagens, ela introduz um desafio familiar: a necessidade de usar blocos try...catch para capturar erros de Promises rejeitadas. O problema se torna evidente em operações encadeadas.
O Problema: Encadeamento de try...catch
Considere a obtenção sequencial de dados de uma API. Uma abordagem ingênua resulta em código profundamente aninhado:
async function obterDadosPagina(idUsuario) {
try {
const usuario = await buscarUsuario(idUsuario);
console.log('Nome:', usuario.nome);
try {
const postagens = await buscarPostagens(usuario.id);
console.log('Postagens:', postagens);
try {
const comentarios = await buscarComentarios(postagens[0].id);
console.log('Primeiro comentário:', comentarios[0]);
} catch (erroComentario) {
console.error('Falha ao buscar comentários:', erroComentario);
}
} catch (erroPostagem) {
console.error('Falha ao buscar postagens:', erroPostagem);
}
} catch (erroUsuario) {
console.error('Falha ao buscar usuário:', erroUsuario);
}
}
Este estilo apresenta vários problemas: código verboso, legibilidade prejudicada pelo excesso de aninhamento e mistura da lógica de sucesso com a de tratamento de erros.
A Solução: Padrão de Erros no Estilo Go
Podemos adotar um padrão inspirado na linguagem Go, onde as funções retornam um tuple [resultado, erro]. Podemos criar uma função auxiliar que transforma uma Promise em uma Promise que sempre resolve com um tuple [error, data]. Se a Promise original for resolvida, retorna [null, data]. Se for rejeitada, retorna [erro, undefined].
// utils/asyncHelper.js
export function tratarAsync(promise) {
return promise
.then(dados => [null, dados])
.catch(erro => [erro, undefined]);
}
Ao usar essa função auxiliar, a lógica de tratamento de erros se torna uma verificação condicional simples. Veja a refatoração da primeira função:
import { tratarAsync } from './utils/asyncHelper';
async function mostrarPerfil(idUsuario) {
const [erro, usuario] = await tratarAsync(buscarUsuario(idUsuario));
if (erro) {
console.error('Falha ao buscar perfil:', erro);
return; // Cláusula de guarda - saia antecipadamente.
}
// O "caminho feliz" está no nível superior, sem aninhamento.
console.log('Perfil carregado:', usuario);
// ... lógica adicional
}
Refatorando o Código Encadeado
Aplicando o mesmo padrão ao exemplo complexo anterior, eliminamos completamente o aninhamento:
import { tratarAsync } from './utils/asyncHelper';
async function carregarConteudoPagina(idUsuario) {
const [erroUser, user] = await tratarAsync(buscarUsuario(idUsuario));
if (erroUser) {
return console.error('Falha no usuário:', erroUser);
}
console.log('Usuário:', user.nome);
const [erroPosts, posts] = await tratarAsync(buscarPostagens(user.id));
if (erroPosts) {
return console.error('Falha nas postagens:', erroPosts);
}
console.log('Postagens encontradas:', posts.length);
const [erroComent, comentarios] = await tratarAsync(buscarComentarios(posts[0].id));
if (erroComent) {
return console.error('Falha nos comentários:', erroComent);
}
console.log('Comentário:', comentarios[0].texto);
}
O código agora é linear, legível e cada etapa de tratamento de erros é claramente delimitada.
Vantagens do Padrão
- Código Mais Limpo: Elimina a "pirâmide de doom" (pyramid of doom), mantendo a lógica principle no escopo principal.
- Menos Código Repetitivo: Encapsula a conversão de rejeição em uma função reutilizável.
- Tratamento de Erros Explícito: A desestruturação
const [error, data]torna obrigatório o manuseio do erro, reduzindo a chance de erros serem igonrados. - Separação de Responsabilidades: As cláusulas de guarda (guard clauses) separam nitidamente o fluxo de erro do fluxo de sucesso.
Uso com Promise.all
O padrão também é eficaz para operações concorrentes. Ele permite tratar cenários onde algumas promises são resolvidas e outras rejeitadas:
async function carregarDashboard(idUsuario) {
const [
[erroUser, dadosUser],
[erroConfigs, dadosConfigs]
] = await Promise.all([
tratarAsync(buscarUsuario(idUsuario)),
tratarAsync(buscarConfiguracoes(idUsuario))
]);
if (erroUser) {
console.error('Não foi possível carregar os dados do usuário.');
}
if (erroConfigs) {
console.error('Não foi possível carregar as configurações.');
}
// Dados parciais ainda podem ser usados
if (dadosUser) {
// ... inicializar parte do dashboard
}
if (dadosConfigs) {
// ... aplicar tema do usuário
}
}
O bloco try...catch permanece sendo o mecanismo fundamental. A estratégia é encapsulá-lo em uma abstração como a função tratarAsync, resultando em código de aplicação mais declarativo e menos propenso a erros.