Error Boundaries e tratamento de falhas no frontend
1. Fundamentos do Tratamento de Erros em React
1.1. Erros em JavaScript: tipos, propagação e o ciclo de vida no React
Erros em JavaScript podem ser classificados em três categorias principais: erros de sintaxe (detectados em tempo de compilação), erros de tempo de execução (como TypeError ou ReferenceError) e erros lógicos (que não lançam exceções, mas produzem resultados incorretos). No React, erros durante a renderização, em métodos de ciclo de vida ou em construtores de componentes podem quebrar toda a árvore de componentes, resultando em uma tela branca e uma experiência de usuário catastrófica.
// Exemplo de erro que quebra a aplicação
function ComponentePropenso() {
const dados = null;
return <div>{dados.propriedadeInexistente}</div>; // TypeError
}
1.2. Limitações do try/catch no contexto de componentes React
O try/catch tradicional funciona bem para código imperativo, mas falha em capturar erros durante a renderização de componentes React. Isso ocorre porque o React usa uma abordagem declarativa e o ciclo de renderização é gerenciado internamente pelo framework.
function App() {
try {
return <ComponentePropenso />; // try/catch NÃO captura este erro
} catch (erro) {
return <div>Erro capturado: {erro.message}</div>; // Isso nunca executa
}
}
1.3. O papel do Error Boundary na arquitetura de componentes
Error Boundaries são componentes React que capturam erros JavaScript em qualquer lugar da árvore de componentes abaixo deles, registram esses erros e exibem uma UI de fallback em vez da árvore de componentes que quebrou. Eles são a única maneira nativa de lidar com erros de renderização em React.
2. Implementando Error Boundaries Clássicos
2.1. Métodos de ciclo de vida: static getDerivedStateFromError() e componentDidCatch()
Para criar um Error Boundary clássico, utilizamos dois métodos específicos do ciclo de vida:
- static getDerivedStateFromError(error): atualiza o estado para renderizar a UI de fallback
- componentDidCatch(error, errorInfo): usado para logging do erro
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Erro capturado:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback || <h1>Algo deu errado.</h1>;
}
return this.props.children;
}
}
2.2. Criando um componente ErrorBoundary reutilizável
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// Enviar para serviço de logging
logErrorToService(error, errorInfo);
}
handleRetry = () => {
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError) {
return (
<div role="alert">
<h2>Ops! Encontramos um problema.</h2>
<p>{this.state.error?.message}</p>
<button onClick={this.handleRetry}>Tentar novamente</button>
</div>
);
}
return this.props.children;
}
}
2.3. Estratégias de fallback UI: mensagens, botões de retry e logging
A UI de fallback deve ser informativa e acionável. Inclua botões de retry, mensagens claras e, se possível, um link para suporte.
<ErrorBoundary fallback={<CustomFallback />}>
<Dashboard />
</ErrorBoundary>
3. Error Boundaries com React Hooks
3.1. A falta de hooks nativos para Error Boundaries e a solução com react-error-boundary
React não fornece hooks nativos para Error Boundaries porque eles exigem métodos de ciclo de vida de classe. O pacote react-error-boundary preenche essa lacuna oferecendo uma API baseada em hooks.
3.2. Usando useErrorHandler e ErrorBoundary do pacote react-error-boundary
import { ErrorBoundary, useErrorHandler } from 'react-error-boundary';
function ComponenteComErro() {
const handleError = useErrorHandler();
const fazerRequisicao = async () => {
try {
await fetchDados();
} catch (erro) {
handleError(erro); // Delega o erro ao ErrorBoundary mais próximo
}
};
return <button onClick={fazerRequisicao}>Carregar dados</button>;
}
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, info) => logError(error, info)}
onReset={() => limparEstado()}
>
<ComponenteComErro />
</ErrorBoundary>
);
}
3.3. Custom hooks para resetar estado e re-renderizar após falha
function useErrorBoundary() {
const [error, setError] = useState(null);
const handleError = useCallback((erro) => {
setError(erro);
}, []);
const resetError = useCallback(() => {
setError(null);
}, []);
return { error, handleError, resetError };
}
4. Tratamento de Erros em Requisições Assíncronas
4.1. Gerenciamento de erros em fetch e axios com interceptors
// Axios interceptor global
axios.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
redirecionarParaLogin();
}
return Promise.reject(error);
}
);
// Fetch com tratamento de erro
async function fetchComTratamento(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (erro) {
console.error('Falha na requisição:', erro);
throw erro;
}
}
4.2. Tratamento de erros em React Query e SWR
// React Query
const { data, error, isLoading, refetch } = useQuery('dados', fetchDados, {
retry: 3,
retryDelay: attempt => Math.min(1000 * 2 ** attempt, 30000),
onError: (error) => {
mostrarToast(`Erro: ${error.message}`);
}
});
// SWR
const { data, error, mutate } = useSWR('/api/dados', fetcher, {
onError: (error) => {
logError(error);
},
onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
if (retryCount >= 3) return;
setTimeout(() => revalidate({ retryCount }), 5000);
}
});
4.3. Exibindo erros de API no frontend com toasts e banners contextuais
function ErrorBanner({ error, onRetry }) {
if (!error) return null;
return (
<div className="error-banner" role="alert">
<span>{error.message}</span>
<button onClick={onRetry}>Tentar novamente</button>
</div>
);
}
5. Logging e Monitoramento de Erros no Frontend
5.1. Integrando Error Boundaries com serviços de logging (Sentry, LogRocket)
import * as Sentry from '@sentry/react';
Sentry.init({
dsn: 'https://seu-dsn@sentry.io/project-id',
environment: process.env.NODE_ENV,
integrations: [new Sentry.BrowserTracing()]
});
// ErrorBoundary com Sentry
class SentryErrorBoundary extends React.Component {
componentDidCatch(error, errorInfo) {
Sentry.captureException(error, { extra: errorInfo });
}
// ... resto da implementação
}
5.2. Enriquecendo logs com contexto: user, ação, estado do componente
function logError(error, context = {}) {
const enrichedError = {
error: error.message,
stack: error.stack,
timestamp: new Date().toISOString(),
user: getUserContext(),
action: context.action,
componentState: context.state,
url: window.location.href
};
// Enviar para serviço de logging
fetch('/api/logs', {
method: 'POST',
body: JSON.stringify(enrichedError)
});
}
5.3. Estratégias de agregação e filtragem para evitar sobrecarga de logs
Implemente debouncing para erros repetidos e agregação por tipo de erro:
const errorCache = new Map();
function logErrorWithDedup(error, maxOccurrences = 10) {
const key = `${error.name}:${error.message}`;
const count = (errorCache.get(key) || 0) + 1;
if (count <= maxOccurrences) {
errorCache.set(key, count);
sendToLoggingService(error);
}
}
6. Estratégias de Fallback e Recuperação
6.1. Fallbacks granulares: Error Boundaries aninhados por seção da página
function Pagina() {
return (
<ErrorBoundary fallback={<HeaderFallback />}>
<Header />
<ErrorBoundary fallback={<SidebarFallback />}>
<Sidebar />
<ErrorBoundary fallback={<ContentFallback />}>
<MainContent />
</ErrorBoundary>
</ErrorBoundary>
<Footer />
</ErrorBoundary>
);
}
6.2. Padrão de retry com backoff exponencial no frontend
async function retryWithBackoff(fn, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries - 1) throw error;
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
6.3. Recuperação de estado: resetando Error Boundaries e mantendo dados intactos
function useErrorRecovery(initialState) {
const [state, setState] = useState(initialState);
const [error, setError] = useState(null);
const reset = useCallback(() => {
setError(null);
setState(initialState);
}, [initialState]);
return { state, setState, error, setError, reset };
}
7. Testando Tratamento de Erros
7.1. Simulando erros em testes unitários com Jest e Testing Library
import { render, screen } from '@testing-library/react';
test('exibe fallback quando componente quebra', () => {
const ComponenteQuebrado = () => {
throw new Error('Erro simulado');
};
render(
<ErrorBoundary fallback={<div>Erro capturado</div>}>
<ComponenteQuebrado />
</ErrorBoundary>
);
expect(screen.getByText('Erro capturado')).toBeInTheDocument();
});
7.2. Testando o comportamento de Error Boundaries
test('chama callback onError quando ocorre erro', () => {
const onError = jest.fn();
const ComponenteQuebrado = () => {
throw new Error('Erro de teste');
};
render(
<ErrorBoundary onError={onError} fallback={<div>Fallback</div>}>
<ComponenteQuebrado />
</ErrorBoundary>
);
expect(onError).toHaveBeenCalledTimes(1);
expect(onError).toHaveBeenCalledWith(expect.any(Error), expect.any(Object));
});
7.3. Testes de integração para fluxos de erro com mocks de API
import { rest } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
rest.get('/api/dados', (req, res, ctx) => {
return res(ctx.status(500), ctx.json({ message: 'Erro interno' }));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('exibe toast de erro quando API falha', async () => {
render(<Dashboard />);
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent('Erro interno');
});
});
Referências
- Error Boundaries - Documentação Oficial React — Documentação oficial sobre Error Boundaries, incluindo limitações e boas práticas de implementação.
- react-error-boundary - npm — Pacote que oferece Error Boundaries baseados em hooks para React moderno.
- Sentry React Error Boundary Integration — Guia oficial de integração do Sentry com Error Boundaries para monitoramento de erros.
- Testing Error Boundaries with React Testing Library — Exemplos práticos de como testar Error Boundaries usando Testing Library.
- Error Handling in React Query — Documentação oficial sobre tratamento de erros em React Query, incluindo retry e callbacks de erro.