Tratamento de erros assíncronos no JavaScript

1. Fundamentos do tratamento de erros em operações assíncronas

1.1. Diferenças entre erros síncronos e assíncronos

O try/catch tradicional só captura erros que ocorrem no mesmo "loop de eventos" (event loop). Erros que acontecem dentro de callbacks, Promises ou operações assíncronas escapam desse bloco:

// Isso NÃO captura um erro assíncrono
try {
  setTimeout(() => {
    throw new Error('Falha assíncrona');
  }, 1000);
} catch (err) {
  console.error('Capturado:', err); // Isso nunca será executado
}

// O erro acima quebra a aplicação com "Uncaught Error"

1.2. Callbacks e o padrão "error-first"

No Node.js, o padrão clássico para callbacks assíncronos é o "error-first":

const fs = require('fs');

fs.readFile('/arquivo-inexistente.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('Erro ao ler arquivo:', err.message);
    return;
  }
  console.log('Conteúdo:', data);
});

1.3. O problema de errar silenciosamente

Erros não tratados em callbacks e Promises podem passar despercebidos:

// Promise rejeitada sem tratamento
const promessaPerigosa = new Promise((_, reject) => {
  reject(new Error('Rejeitada sem .catch()'));
});

// Isso gera um "UnhandledPromiseRejection" no Node.js
// Silenciosamente falha no navegador (depende do ambiente)

2. Tratamento de erros com Promises

2.1. .catch() em cadeias de Promises

fetch('https://api.exemplo.com/dados')
  .then(response => response.json())
  .then(data => {
    console.log('Dados recebidos:', data);
  })
  .catch(err => {
    console.error('Falha na requisição:', err.message);
  });

2.2. Propagação de erros em then()

Erros se propagam automaticamente pela cadeia de Promises:

Promise.resolve(42)
  .then(valor => {
    throw new Error('Erro no primeiro then');
  })
  .then(() => {
    // Este bloco NUNCA será executado
    console.log('Isso não aparece');
  })
  .catch(err => {
    console.error('Capturado no final:', err.message);
    // "Capturado no final: Erro no primeiro then"
  });

2.3. Operações paralelas e tratamento de erros

const promises = [
  fetch('/api/usuario'),
  fetch('/api/pedidos'),
  fetch('/api/produtos')
];

// Promise.all falha no primeiro erro
Promise.all(promises)
  .then(respostas => Promise.all(respostas.map(r => r.json())))
  .catch(err => console.error('Uma requisição falhou:', err));

// Promise.allSettled coleta todos os resultados (sucesso ou falha)
Promise.allSettled(promises)
  .then(resultados => {
    resultados.forEach((r, i) => {
      if (r.status === 'rejected') {
        console.error(`Promise ${i} rejeitada:`, r.reason);
      }
    });
  });

3. Tratamento de erros com async/await

3.1. try/catch com await

async function buscarDados() {
  try {
    const response = await fetch('https://api.exemplo.com/dados');
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    const data = await response.json();
    return data;
  } catch (err) {
    console.error('Erro na operação:', err.message);
    throw err; // Re-lança para quem chamou tratar
  }
}

3.2. Tratamento granular vs. centralizado

// Abordagem granular: trata erros perto da origem
async function salvarUsuario(usuario) {
  try {
    await db.insert('usuarios', usuario);
  } catch (err) {
    if (err.code === 'ER_DUP_ENTRY') {
      throw new Error('Usuário já existe');
    }
    throw new Error('Falha ao salvar usuário');
  }
}

// Abordagem centralizada: trata erros no nível mais alto
async function handler(req, res) {
  try {
    const usuario = await salvarUsuario(req.body);
    res.json(usuario);
  } catch (err) {
    res.status(400).json({ erro: err.message });
  }
}

3.3. Erros em loops assíncronos

async function processarItens(itens) {
  const resultados = [];

  for (const item of itens) {
    try {
      const resultado = await fetch(`/api/processar/${item}`);
      resultados.push(await resultado.json());
    } catch (err) {
      console.error(`Falha ao processar ${item}:`, err.message);
      // Continua com o próximo item
    }
  }

  return resultados;
}

4. Erros não capturados e eventos globais

4.1. process.on('unhandledRejection') no Node.js

process.on('unhandledRejection', (reason, promise) => {
  console.error('Promise rejeitada sem tratamento:', reason);
  // Idealmente, registrar em sistema de log
});

4.2. process.on('uncaughtException')

process.on('uncaughtException', (err) => {
  console.error('Erro não capturado:', err);
  // Limpar recursos e finalizar graciosamente
  process.exit(1);
});

4.3. No navegador/React

// No navegador
window.addEventListener('unhandledrejection', (event) => {
  console.error('Promise rejeitada:', event.reason);
  event.preventDefault(); // Evita log no console
});

// No React, combine com Error Boundary

5. Tratamento de erros em APIs e requisições HTTP

5.1. Padrões com fetch e axios

// Com fetch
async function buscarComFetch(url) {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`Erro HTTP ${response.status}: ${response.statusText}`);
  }
  return response.json();
}

// Com axios (trata 4xx e 5xx como erro automaticamente)
import axios from 'axios';

async function buscarComAxios(url) {
  try {
    const { data } = await axios.get(url);
    return data;
  } catch (err) {
    if (err.response) {
      console.error('Status:', err.response.status);
      console.error('Dados:', err.response.data);
    } else if (err.request) {
      console.error('Sem resposta do servidor');
    } else {
      console.error('Erro na configuração:', err.message);
    }
    throw err;
  }
}

5.2. Timeout e cancelamento com AbortController

function buscarComTimeout(url, timeoutMs = 5000) {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), timeoutMs);

  return fetch(url, { signal: controller.signal })
    .then(response => {
      clearTimeout(timeout);
      return response.json();
    })
    .catch(err => {
      clearTimeout(timeout);
      if (err.name === 'AbortError') {
        throw new Error('Requisição cancelada por timeout');
      }
      throw err;
    });
}

5.3. Retry com backoff

async function fetchComRetry(url, maxRetries = 3) {
  for (let tentativa = 1; tentativa <= maxRetries; tentativa++) {
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return await response.json();
    } catch (err) {
      if (tentativa === maxRetries) throw err;

      const delay = Math.pow(2, tentativa) * 1000; // Backoff exponencial
      console.log(`Tentativa ${tentativa} falhou. Tentando novamente em ${delay}ms...`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

6. Tratamento de erros assíncronos no ecossistema React

6.1. Error Boundaries e componentes assíncronos

// Error Boundary (classe) não captura erros em event handlers assíncronos
class ErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    console.error('Erro capturado:', error, info);
  }

  render() {
    if (this.state.hasError) {
      return <h1>Algo deu errado</h1>;
    }
    return this.props.children;
  }
}

6.2. Tratamento em hooks

function useDados(url) {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let cancelled = false;

    async function fetchData() {
      try {
        const response = await fetch(url);
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const result = await response.json();
        if (!cancelled) setData(result);
      } catch (err) {
        if (!cancelled) setError(err);
      } finally {
        if (!cancelled) setLoading(false);
      }
    }

    fetchData();
    return () => { cancelled = true; };
  }, [url]);

  return { data, error, loading };
}

6.3. Gerenciamento com React Query

import { useQuery } from '@tanstack/react-query';

function Usuarios() {
  const { data, error, isLoading, refetch } = useQuery({
    queryKey: ['usuarios'],
    queryFn: async () => {
      const res = await fetch('/api/usuarios');
      if (!res.ok) throw new Error('Falha ao carregar usuários');
      return res.json();
    },
    retry: 3,
    retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
  });

  if (isLoading) return <div>Carregando...</div>;
  if (error) return <div>Erro: {error.message}</div>;

  return <div>{/* renderizar dados */}</div>;
}

7. Boas práticas e padrões avançados

7.1. Wrappers de erro

function asyncHandler(fn) {
  return function(...args) {
    return fn(...args).catch(err => {
      console.error('Erro na função assíncrona:', err);
      // Lógica centralizada de erro
    });
  };
}

const buscarSeguro = asyncHandler(async (url) => {
  const response = await fetch(url);
  return response.json();
});

7.2. Erros customizados

class ApiError extends Error {
  constructor(status, message) {
    super(message);
    this.name = 'ApiError';
    this.status = status;
    this.timestamp = new Date().toISOString();
  }
}

// Uso
throw new ApiError(404, 'Recurso não encontrado');

7.3. Logging e monitoramento

// Integração com Sentry
import * as Sentry from '@sentry/react';

async function operacaoCritica() {
  try {
    return await operacaoArriscada();
  } catch (err) {
    Sentry.captureException(err);
    throw err; // Re-lança para tratamento local
  }
}

8. Armadilhas comuns e depuração

8.1. Promises "órfãs"

// PROBLEMA: Promise não retornada
async function processar() {
  fetch('/api/dados').then(data => {
    console.log(data);
  });
  // Se fetch rejeitar, o erro fica órfão
}

// SOLUÇÃO: Sempre retornar ou usar await
async function processar() {
  try {
    const data = await fetch('/api/dados');
    console.log(data);
  } catch (err) {
    console.error(err);
  }
}

8.2. Erros em setTimeout e event emitters

// setTimeout não captura rejeições
setTimeout(() => {
  Promise.reject(new Error('Erro no timeout'));
}, 1000);
// Isso gera unhandledRejection

// SOLUÇÃO: Tratar dentro do callback
setTimeout(() => {
  Promise.reject(new Error('Erro no timeout'))
    .catch(err => console.error('Capturado:', err));
}, 1000);

8.3. Stack traces assíncronos

// Node.js com async stack traces (--async-stack-traces)
async function nivel1() {
  await nivel2();
}

async function nivel2() {
  throw new Error('Erro no nível 2');
}

nivel1().catch(err => {
  console.error(err.stack);
  // Mostra a cadeia completa de chamadas assíncronas
});

Referências