Um guia completo para entender e implementar Error Boundaries de JavaScript em React para um tratamento de erros robusto e degradação graciosa da UI.
Error Boundary em JavaScript: Um Guia de Implementação para Tratamento de Erros em React
No universo do desenvolvimento React, erros inesperados podem levar a experiências de usuário frustrantes e instabilidade da aplicação. Uma estratégia bem definida de tratamento de erros é crucial para construir aplicações robustas e confiáveis. Os Error Boundaries do React fornecem um mecanismo poderoso para tratar elegantemente os erros que ocorrem na sua árvore de componentes, evitando que toda a aplicação quebre e permitindo que você exiba uma UI de fallback.
O que é um Error Boundary?
Um Error Boundary é um componente React que captura erros de JavaScript em qualquer lugar da sua árvore de componentes filhos, registra esses erros e exibe uma UI de fallback em vez da árvore de componentes que quebrou. Os Error Boundaries capturam erros durante a renderização, em métodos de ciclo de vida e nos construtores de toda a árvore abaixo deles.
Pense em um Error Boundary como um bloco try...catch
para componentes React. Assim como um bloco try...catch
permite que você trate exceções em código JavaScript síncrono, um Error Boundary permite que você trate erros que ocorrem durante a renderização dos seus componentes React.
Nota Importante: Error Boundaries não capturam erros para:
- Manipuladores de eventos (saiba mais nas seções seguintes)
- Código assíncrono (ex: callbacks de
setTimeout
ourequestAnimationFrame
) - Renderização no lado do servidor (Server-side rendering)
- Erros lançados no próprio Error Boundary (em vez de em seus filhos)
Por que usar Error Boundaries?
Usar Error Boundaries oferece várias vantagens significativas:
- Experiência do Usuário Melhorada: Em vez de exibir uma tela branca em branco ou uma mensagem de erro enigmática, você pode mostrar uma UI de fallback amigável, informando ao usuário que algo deu errado e potencialmente oferecendo uma maneira de se recuperar (por exemplo, recarregando a página ou navegando para uma seção diferente).
- Estabilidade da Aplicação: Os Error Boundaries evitam que erros em uma parte da sua aplicação quebrem a aplicação inteira. Isso é particularmente importante para aplicações complexas com muitos componentes interconectados.
- Tratamento de Erros Centralizado: Os Error Boundaries fornecem um local centralizado para registrar erros e rastrear a causa raiz dos problemas. Isso simplifica a depuração e a manutenção.
- Degradação Graciosa: Você pode posicionar estrategicamente Error Boundaries em diferentes partes da sua aplicação para garantir que, mesmo que alguns componentes falhem, o restante da aplicação permaneça funcional. Isso permite uma degradação graciosa diante de erros.
Implementando Error Boundaries em React
Para criar um Error Boundary, você precisa definir um componente de classe que implemente um (ou ambos) dos seguintes métodos de ciclo de vida:
static getDerivedStateFromError(error)
: Este método de ciclo de vida é chamado depois que um erro é lançado por um componente descendente. Ele recebe o erro que foi lançado como argumento e deve retornar um valor para atualizar o estado do componente para indicar que um erro ocorreu (por exemplo, definindo uma flaghasError
comotrue
).componentDidCatch(error, info)
: Este método de ciclo de vida é chamado depois que um erro é lançado por um componente descendente. Ele recebe o erro que foi lançado como argumento, junto com um objetoinfo
contendo informações sobre qual componente lançou o erro. Você pode usar este método para registrar o erro em um serviço como Sentry ou Bugsnag.
Aqui está um exemplo básico de um componente Error Boundary:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null
};
}
static getDerivedStateFromError(error) {
// Atualiza o estado para que a próxima renderização mostre a UI de fallback.
return {
hasError: true,
error: error
};
}
componentDidCatch(error, info) {
// Exemplo de "componentStack":
// in ComponentThatThrows (created by App)
// in MyErrorBoundary (created by App)
// in div (created by App)
// in App
console.error("Caught an error:", error, info);
this.setState({
errorInfo: info.componentStack
});
// Você também pode registrar o erro em um serviço de relatórios de erros
//logErrorToMyService(error, info.componentStack);
}
render() {
if (this.state.hasError) {
// Você pode renderizar qualquer UI de fallback personalizada
return (
<div>
<h2>Algo deu errado.</h2>
<p>Erro: {this.state.error ? this.state.error.message : "Ocorreu um erro desconhecido."}</p>
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.errorInfo && this.state.errorInfo}
</details>
</div>
);
}
return this.props.children;
}
}
Para usar o Error Boundary, simplesmente envolva a árvore de componentes que você deseja proteger:
<ErrorBoundary>
<MyComponentThatMightThrow/>
</ErrorBoundary>
Exemplos Práticos de Uso de Error Boundary
Vamos explorar alguns cenários práticos onde os Error Boundaries podem ser particularmente úteis:
1. Tratando Erros de API
Ao buscar dados de uma API, erros podem ocorrer devido a problemas de rede, problemas no servidor ou dados inválidos. Você pode envolver o componente que busca e exibe os dados com um Error Boundary para tratar esses erros de forma elegante.
function UserProfile() {
const [user, setUser] = React.useState(null);
const [isLoading, setIsLoading] = React.useState(true);
React.useEffect(() => {
async function fetchData() {
try {
const response = await fetch('/api/user');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setUser(data);
} catch (error) {
// O erro será capturado pelo ErrorBoundary
throw error;
} finally {
setIsLoading(false);
}
}
fetchData();
}, []);
if (isLoading) {
return <p>Carregando perfil do usuário...</p>;
}
if (!user) {
return <p>Nenhum dado de usuário disponível.</p>;
}
return (
<div>
<h2>{user.name}</h2>
<p>Email: {user.email}</p>
</div>
);
}
function App() {
return (
<ErrorBoundary>
<UserProfile />
</ErrorBoundary>
);
}
Neste exemplo, se a chamada da API falhar ou retornar um erro, o Error Boundary capturará o erro e exibirá uma UI de fallback (definida dentro do método render
do Error Boundary). Isso evita que toda a aplicação quebre e fornece ao usuário uma mensagem mais informativa. Você poderia expandir a UI de fallback para fornecer uma opção de tentar a requisição novamente.
2. Tratando Erros de Bibliotecas de Terceiros
Ao usar bibliotecas de terceiros, é possível que elas lancem erros inesperados. Envolver componentes que usam essas bibliotecas com Error Boundaries pode ajudá-lo a tratar esses erros de forma elegante.
Considere uma biblioteca de gráficos hipotética que ocasionalmente lança erros devido a inconsistências de dados ou outros problemas. Você poderia envolver o componente de gráfico da seguinte forma:
function MyChartComponent() {
try {
// Renderiza o gráfico usando a biblioteca de terceiros
return <Chart data={data} />;
} catch (error) {
// Este bloco catch não será eficaz para erros de ciclo de vida de componentes React
// É principalmente para erros síncronos dentro desta função específica.
console.error("Error rendering chart:", error);
// Considere lançar o erro para ser capturado pelo ErrorBoundary
throw error; // Relançando o erro
}
}
function App() {
return (
<ErrorBoundary>
<MyChartComponent />
</ErrorBoundary>
);
}
Se o componente Chart
lançar um erro, o Error Boundary o capturará e exibirá uma UI de fallback. Note que o try/catch dentro de MyChartComponent só capturará erros dentro da função síncrona, não no ciclo de vida do componente. Portanto, o ErrorBoundary é crítico aqui.
3. Tratando Erros de Renderização
Erros podem ocorrer durante o processo de renderização devido a dados inválidos, tipos de props incorretos ou outros problemas. Os Error Boundaries podem capturar esses erros e evitar que a aplicação quebre.
function DisplayName({ name }) {
if (typeof name !== 'string') {
throw new Error('Name must be a string');
}
return <h2>Hello, {name}!</h2>;
}
function App() {
return (
<ErrorBoundary>
<DisplayName name={123} /> <!-- Tipo de prop incorreto -->
</ErrorBoundary>
);
}
Neste exemplo, o componente DisplayName
espera que a prop name
seja uma string. Se um número for passado em vez disso, um erro será lançado, e o Error Boundary o capturará e exibirá uma UI de fallback.
Error Boundaries e Manipuladores de Eventos
Como mencionado anteriormente, Error Boundaries não capturam erros que ocorrem dentro de manipuladores de eventos. Isso ocorre porque os manipuladores de eventos são tipicamente assíncronos, e os Error Boundaries só capturam erros que ocorrem durante a renderização, em métodos de ciclo de vida e em construtores.
Para tratar erros em manipuladores de eventos, você precisa usar um bloco try...catch
tradicional dentro da função do manipulador de eventos.
function MyComponent() {
const handleClick = () => {
try {
// Algum código que pode lançar um erro
throw new Error('An error occurred in the event handler');
} catch (error) {
console.error('Caught an error in the event handler:', error);
// Trate o erro (ex: exiba uma mensagem de erro para o usuário)
}
};
return <button onClick={handleClick}>Click Me</button>;
}
Tratamento Global de Erros
Embora os Error Boundaries sejam excelentes para tratar erros dentro da árvore de componentes do React, eles não cobrem todos os cenários de erro possíveis. Por exemplo, eles não capturam erros que ocorrem fora dos componentes React, como erros em ouvintes de eventos globais ou erros em código que é executado antes do React ser inicializado.
Para tratar esses tipos de erros, você pode usar o manipulador de eventos window.onerror
.
window.onerror = function(message, source, lineno, colno, error) {
console.error('Global error handler:', message, source, lineno, colno, error);
// Registre o erro em um serviço como Sentry ou Bugsnag
// Exiba uma mensagem de erro global para o usuário (opcional)
return true; // Previne o comportamento padrão de tratamento de erros
};
O manipulador de eventos window.onerror
é chamado sempre que um erro de JavaScript não capturado ocorre. Você pode usá-lo para registrar o erro, exibir uma mensagem de erro global para o usuário ou tomar outras ações para tratar o erro.
Importante: Retornar true
do manipulador de eventos window.onerror
impede que o navegador exiba a mensagem de erro padrão. No entanto, esteja atento à experiência do usuário; se você suprimir a mensagem padrão, certifique-se de fornecer uma alternativa clara e informativa.
Melhores Práticas para Usar Error Boundaries
Aqui estão algumas melhores práticas a serem lembradas ao usar Error Boundaries:
- Posicione os Error Boundaries estrategicamente: Envolva diferentes partes da sua aplicação com Error Boundaries para isolar erros e evitar que eles se propaguem em cascata. Considere envolver rotas inteiras ou seções principais da sua UI.
- Forneça uma UI de fallback informativa: A UI de fallback deve informar ao usuário que ocorreu um erro e, potencialmente, oferecer uma maneira de se recuperar. Evite exibir mensagens de erro genéricas como "Algo deu errado."
- Registre os erros: Use o método de ciclo de vida
componentDidCatch
para registrar erros em um serviço como Sentry ou Bugsnag. Isso ajudará você a rastrear a causa raiz dos problemas e a melhorar a estabilidade da sua aplicação. - Não use Error Boundaries para erros esperados: Error Boundaries são projetados para tratar erros inesperados. Para erros esperados (ex: erros de validação, erros de API), use mecanismos de tratamento de erros mais específicos, como blocos
try...catch
ou componentes de tratamento de erros personalizados. - Considere múltiplos níveis de Error Boundaries: Você pode aninhar Error Boundaries para fornecer diferentes níveis de tratamento de erros. Por exemplo, você pode ter um Error Boundary global que captura quaisquer erros não tratados e exibe uma mensagem de erro genérica, e Error Boundaries mais específicos que capturam erros em componentes específicos e exibem mensagens de erro mais detalhadas.
- Não se esqueça da renderização no lado do servidor: Se você estiver usando renderização no lado do servidor, precisará tratar os erros no servidor também. Os Error Boundaries funcionam no servidor, mas você pode precisar usar mecanismos de tratamento de erros adicionais para capturar erros que ocorrem durante a renderização inicial.
Técnicas Avançadas de Error Boundary
1. Usando uma Render Prop
Em vez de renderizar uma UI de fallback estática, você pode usar uma render prop para fornecer mais flexibilidade na forma como os erros são tratados. Uma render prop é uma prop de função que um componente usa para renderizar algo.
class ErrorBoundary extends React.Component {
// ... (o mesmo de antes)
render() {
if (this.state.hasError) {
// Usa a render prop para renderizar a UI de fallback
return this.props.fallbackRender(this.state.error, this.state.errorInfo);
}
return this.props.children;
}
}
function App() {
return (
<ErrorBoundary fallbackRender={(error, errorInfo) => (
<div>
<h2>Algo deu errado!</h2>
<p>Erro: {error.message}</p>
<details style={{ whiteSpace: 'pre-wrap' }}>
{errorInfo.componentStack}
</details>
</div>
)}>
<MyComponentThatMightThrow/>
</ErrorBoundary>
);
}
Isso permite que você personalize a UI de fallback para cada Error Boundary. A prop fallbackRender
recebe o erro e as informações do erro como argumentos, permitindo que você exiba mensagens de erro mais específicas ou tome outras ações com base no erro.
2. Error Boundary como um Componente de Ordem Superior (HOC)
Você pode criar um componente de ordem superior (HOC) que envolve outro componente com um Error Boundary. Isso pode ser útil para aplicar Error Boundaries a múltiplos componentes sem ter que repetir o mesmo código.
function withErrorBoundary(WrappedComponent) {
return class WithErrorBoundary extends React.Component {
render() {
return (
<ErrorBoundary>
<WrappedComponent {...this.props} />
</ErrorBoundary>
);
}
};
}
// Uso:
const MyComponentWithErrorHandling = withErrorBoundary(MyComponentThatMightThrow);
A função withErrorBoundary
recebe um componente como argumento e retorna um novo componente que envolve o componente original com um Error Boundary. Isso permite que você adicione facilmente o tratamento de erros a qualquer componente em sua aplicação.
Testando Error Boundaries
É importante testar seus Error Boundaries para garantir que eles estão funcionando corretamente. Você pode usar bibliotecas de teste como Jest e React Testing Library para testar seus Error Boundaries.
Aqui está um exemplo de como testar um Error Boundary usando a React Testing Library:
import { render, screen, fireEvent } from '@testing-library/react';
import ErrorBoundary from './ErrorBoundary';
function ComponentThatThrows() {
throw new Error('This component throws an error');
}
test('renders fallback UI when an error is thrown', () => {
render(
<ErrorBoundary>
<ComponentThatThrows />
</ErrorBoundary>
);
expect(screen.getByText('Something went wrong.')).toBeInTheDocument();
});
Este teste renderiza o componente ComponentThatThrows
, que lança um erro. O teste então afirma que a UI de fallback renderizada pelo Error Boundary é exibida.
Error Boundaries e Server Components (React 18+)
Com a introdução dos Server Components no React 18 e posteriores, os Error Boundaries continuam a desempenhar um papel vital no tratamento de erros. Os Server Components são executados no servidor e enviam apenas o resultado renderizado para o cliente. Embora os princípios básicos permaneçam os mesmos, existem algumas nuances a serem consideradas:
- Registro de Erros no Lado do Servidor: Certifique-se de que você está registrando os erros que ocorrem nos Server Components no servidor. Isso pode envolver o uso de um framework de logging do lado do servidor ou o envio de erros para um serviço de rastreamento de erros.
- Fallback no Lado do Cliente: Mesmo que os Server Components sejam renderizados no servidor, você ainda precisa fornecer uma UI de fallback no lado do cliente em caso de erros. Isso garante que o usuário tenha uma experiência consistente, mesmo que o servidor falhe ao renderizar o componente.
- Streaming SSR: Ao usar a Renderização no Lado do Servidor (SSR) com streaming, erros podem ocorrer durante o processo de streaming. Os Error Boundaries podem ajudá-lo a tratar esses erros de forma elegante, renderizando uma UI de fallback para o stream afetado.
O tratamento de erros em Server Components é uma área em evolução, por isso é importante manter-se atualizado com as últimas melhores práticas e recomendações.
Armadilhas Comuns a Evitar
- Dependência excessiva de Error Boundaries: Não use Error Boundaries como um substituto para o tratamento de erros adequado em seus componentes. Sempre se esforce para escrever código robusto e confiável que trate os erros de forma elegante.
- Ignorar Erros: Certifique-se de registrar os erros que são capturados pelos Error Boundaries para que você possa rastrear a causa raiz dos problemas. Não simplesmente exiba uma UI de fallback e ignore o erro.
- Usar Error Boundaries para Erros de Validação: Error Boundaries não são a ferramenta certa para tratar erros de validação. Use técnicas de validação mais específicas.
- Não Testar os Error Boundaries: Teste seus Error Boundaries para garantir que eles estão funcionando corretamente.
Conclusão
Error Boundaries são uma ferramenta poderosa para construir aplicações React robustas e confiáveis. Ao entender como implementar e usar Error Boundaries de forma eficaz, você pode melhorar a experiência do usuário, prevenir quebras na aplicação e simplificar a depuração. Lembre-se de posicionar os Error Boundaries estrategicamente, fornecer uma UI de fallback informativa, registrar erros e testar seus Error Boundaries minuciosamente.
Seguindo as diretrizes e melhores práticas descritas neste guia, você pode garantir que suas aplicações React sejam resilientes a erros e forneçam uma experiência positiva para seus usuários.