Uma Abordagem Elegante para Tratamento de Erros em Async/Await

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.

Tags: async-await error-handling TypeScript promise refactoring

Publicado em 6-18 03:38