Using Error Boundaries with Hooks (Part 2) #142
📖 Introduction
In Using Error Boundaries with Hooks (Part 1), we established that while Error Boundaries must be class components, we can effectively use them with functional components by wrapping them or by using libraries like react-error-boundary
. This second part dives deeper into advanced usage with react-error-boundary
, focusing on the useErrorHandler
hook for programmatically triggering error boundaries and discussing more complex patterns and comparisons.
📚 Prerequisites
Before we begin, ensure you have:
- Familiarity with
react-error-boundary
's<ErrorBoundary>
component (covered in Part 1). - Strong understanding of React Hooks, especially
useEffect
and custom Hooks. - Knowledge of asynchronous operations (e.g.,
fetch
API, Promises).
🎯 Article Outline: What You'll Master
In this article, you will learn:
- ✅ The
useErrorHandler
Hook: What it is and why it's useful. - ✅ Triggering Error Boundaries Programmatically: Handling errors from async operations or imperative logic.
- ✅ Practical Example:
useErrorHandler
withfetch
: Catching API errors and delegating to an Error Boundary. - ✅ Higher-Order Component (HOC) Approach: An alternative pattern with
withErrorBoundary
. - ✅ Comparing Approaches: When to use
<ErrorBoundary>
component vs.useErrorHandler
vs.withErrorBoundary
. - ✅ Best Practices for Error Handling in a Hooks World.
🧠 Section 1: The useErrorHandler
Hook from react-error-boundary
The <ErrorBoundary>
component from react-error-boundary
is excellent for catching errors that occur during the React rendering lifecycle within its children. However, what about errors that happen outside of rendering? For example:
- Errors in asynchronous callbacks (e.g.,
.catch()
block of afetch
Promise). - Errors in event handlers that you want to escalate to an Error Boundary.
- Errors in
useEffect
cleanup functions or other imperative code.
Standard JavaScript throw error
in these places won't be caught by React's rendering lifecycle error handling. This is where the useErrorHandler
hook comes in.
What is useErrorHandler
?
useErrorHandler
is a custom hook provided by react-error-boundary
. It gives you a function that, when called with an error object, will propagate that error to the nearest Error Boundary, effectively triggering its fallback UI.
Signature:
import { useErrorHandler } from 'react-error-boundary';
// Inside your functional component:
const handleError = useErrorHandler();
// handleError is now a function you can call with an error object.
If you call handleError(someErrorObject)
, react-error-boundary
ensures this error is thrown in a way that React's error handling mechanism (and thus the nearest <ErrorBoundary>
component from the library or a compatible one) can catch it.
You can also pass a second argument to useErrorHandler
which is an error object. If this error object is not null, useErrorHandler
will immediately throw it. This is useful if an error occurs during the render phase itself.
const [errorForBoundary, setErrorForBoundary] = useState(null);
useErrorHandler(errorForBoundary); // If errorForBoundary is set, it's thrown to the boundary.
💻 Section 2: Triggering Error Boundaries Programmatically
Let's see how useErrorHandler
allows us to send errors from non-rendering contexts to our Error Boundary.
2.1 - Example: useErrorHandler
with an Asynchronous fetch
Call
Imagine a component that fetches data. If the fetch
call fails, we want to display a user-friendly error message using our Error Boundary's fallback UI.
// src/components/DataFetcher.jsx
import React, { useState, useEffect } from 'react';
import { useErrorHandler } from 'react-error-boundary'; // Import the hook
const DataFetcher = ({ url }) => {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const handleError = useErrorHandler(); // Get the error handling function
useEffect(() => {
let isMounted = true; // Prevent state update on unmounted component
setIsLoading(true);
setData(null); // Clear previous data
fetch(url)
.then(response => {
if (!response.ok) {
// Create an error object for non-ok responses
throw new Error(`Network response was not ok: ${response.status} ${response.statusText}`);
}
return response.json();
})
.then(jsonData => {
if (isMounted) {
setData(jsonData);
}
})
.catch(error => {
// If an error occurs (network error or thrown above),
// pass it to handleError to be caught by the ErrorBoundary
if (isMounted) {
handleError(error);
}
})
.finally(() => {
if (isMounted) {
setIsLoading(false);
}
});
return () => { isMounted = false; };
}, [url, handleError]); // Include handleError in dependency array (it's stable)
if (isLoading) {
return <p>Loading data from {url}...</p>;
}
// This component doesn't render its own error state for the fetch.
// It delegates that to the ErrorBoundary via handleError.
// If data is null and not loading, it means either no fetch yet or an error was handled by boundary.
return (
<div>
<h3>Data Loaded:</h3>
<pre>{data ? JSON.stringify(data, null, 2) : "No data (or error occurred)."}</pre>
</div>
);
};
export default DataFetcher;
Using it with an <ErrorBoundary>
:
// src/AppWithAsyncError.jsx
import React from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import DataFetcher from './components/DataFetcher';
// Fallback UI (same as in Part 1)
function ErrorFallbackUI({ error, resetErrorBoundary }) {
return (
<div role="alert" style={{ padding: '20px', border: '1px solid red', margin: '10px', backgroundColor: '#ffe0e0' }}>
<h3>Network or Data Error:</h3>
<pre style={{ color: 'red' }}>{error.message}</pre>
<button onClick={resetErrorBoundary} style={{ padding: '8px 15px' }}>
Try Fetching Again
</button>
</div>
);
}
const myErrorHandlerLogger = (error, info) => {
console.error("App Log (from onError prop):", error, info.componentStack);
};
function AppWithAsyncError() {
// A valid URL for testing success: 'https://jsonplaceholder.typicode.com/todos/1'
// An invalid URL to test error: 'https://jsonplaceholder.typicode.com/nonexistent'
const [dataUrl, setDataUrl] = React.useState('https://jsonplaceholder.typicode.com/todos/1');
const [key, setKey] = React.useState(0); // For resetting via resetKeys
const handleReset = () => {
// Potentially change URL or simply re-trigger fetch by changing key
setKey(prevKey => prevKey + 1);
// If URL caused the error, you might want to prompt user or try a default
// setDataUrl('https://jsonplaceholder.typicode.com/todos/1');
};
return (
<div style={{ padding: '20px' }}>
<h1>Data Fetching with `useErrorHandler`</h1>
<button onClick={() => setDataUrl('https://jsonplaceholder.typicode.com/todos/1')}>Load Valid Data</button>
<button onClick={() => setDataUrl('https://jsonplaceholder.typicode.com/nonexistent')} style={{marginLeft: '10px'}}>Load Invalid Data (to trigger error)</button>
<ErrorBoundary
FallbackComponent={ErrorFallbackUI}
onReset={handleReset}
resetKeys={[dataUrl, key]} // Reset if URL changes or key changes
onError={myErrorHandlerLogger}
>
<DataFetcher url={dataUrl} />
</ErrorBoundary>
</div>
);
}
export default AppWithAsyncError;
Behavior:
DataFetcher
mounts anduseEffect
runs.fetch(url)
is called.- If the
url
is invalid or a network error occurs, the.catch(error => ...)
block is executed. - Inside
.catch()
,handleError(error)
is called. - The
useErrorHandler
hook (viahandleError
) effectively "re-throws" this error in a way that the parent<ErrorBoundary>
component catches it. - The
ErrorBoundary
then rendersErrorFallbackUI
, displaying the network error message. - Clicking "Try Fetching Again" in the fallback calls
resetErrorBoundary
, which in turn callshandleReset
inAppWithAsyncError
. handleReset
changeskey
(and potentiallydataUrl
). Sincekey
is inresetKeys
, theErrorBoundary
resets, andDataFetcher
re-renders, attempting the fetch again with the (potentially new)dataUrl
.
This pattern is extremely useful for centralizing UI error display logic while keeping data fetching components cleaner, as they don't need to manage their own specific error UI states if a global fallback is acceptable.
🛠️ Section 3: The withErrorBoundary
HOC
react-error-boundary
also provides a Higher-Order Component (HOC) called withErrorBoundary
. HOCs are an older pattern in React but still useful in some contexts. withErrorBoundary
wraps your component and effectively places an <ErrorBoundary>
around it.
import React from 'react';
import { withErrorBoundary } from 'react-error-boundary';
import FunctionalWidget from './components/FunctionalWidget'; // Our widget from Part 1
function ErrorFallbackForHOC({ error, resetErrorBoundary }) {
// Same fallback UI component as before
return (
<div role="alert" style={{ padding: '10px', border: '1px solid purple', margin: '10px', backgroundColor: '#f0e0ff' }}>
<h3>Error in HOC-Wrapped Component:</h3>
<pre style={{ color: 'purple' }}>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try HOC Again</button>
</div>
);
}
const myHOCErrorHandlerLogger = (error, info) => {
console.error("HOC Log:", error, info.componentStack);
};
// Wrap FunctionalWidget with the HOC
const ProtectedFunctionalWidget = withErrorBoundary(FunctionalWidget, {
FallbackComponent: ErrorFallbackForHOC,
onError: myHOCErrorHandlerLogger,
// onReset, resetKeys can also be passed here
});
function AppWithHOC() {
const [widgetKey, setWidgetKey] = React.useState(0);
const handleHOCReset = () => {
setWidgetKey(k => k + 1);
};
// To make the HOC resettable via its onReset, we need to pass onReset to it.
// The HOC's options are static, so resetKeys from parent state isn't direct.
// Instead, we can control the key of the HOC-wrapped component.
return (
<div style={{ padding: '20px' }}>
<h1>Using `withErrorBoundary` HOC</h1>
<ProtectedFunctionalWidget
key={widgetKey} // Changing key will remount, effectively resetting
title={`HOC Widget (key: ${widgetKey})`}
// To use onReset from HOC options, the HOC itself would need to accept it
// or be re-instantiated with new options, which is less common.
// The simplest way to reset a HOC-wrapped component is often by changing its key.
/>
<button onClick={handleHOCReset} style={{marginTop: '10px'}}>Reset HOC Widget via Key</button>
</div>
);
}
// If ProtectedFunctionalWidget was defined inside AppWithHOC,
// it could pick up onReset from AppWithHOC's scope for its options.
// Example:
// const ProtectedFunctionalWidget = withErrorBoundary(FunctionalWidget, {
// FallbackComponent: ErrorFallbackForHOC,
// onError: myHOCErrorHandlerLogger,
// onReset: () => setWidgetKey(k => k + 1) // If AppWithHOC state is accessible
// });
export default AppWithHOC;
Usage:
withErrorBoundary(YourComponent, ErrorBoundaryOptions)
:YourComponent
: The component you want to protect.ErrorBoundaryOptions
: An object containing props for the underlying<ErrorBoundary>
(e.g.,FallbackComponent
,onError
,onReset
,resetKeys
).
Resetting with HOCs:
Resetting an HOC-wrapped component where the HOC itself defines the resetKeys
or onReset
can be a bit trickier if those depend on parent state that wasn't available when the HOC was defined.
- Changing the
key
prop on theProtectedFunctionalWidget
(as shown inAppWithHOC
) is a reliable way to force a remount and thus a reset of both the component and its HOC-provided error boundary instance. - If the HOC is defined within a component where it can access parent state setters for its
onReset
option, that's another way.
While HOCs are less common with the prevalence of Hooks, withErrorBoundary
can be useful for applying error handling to existing class components or in situations where a HOC pattern is preferred.
🔬 Section 4: Comparing Approaches
Let's summarize the three main ways to use react-error-boundary
(or similar logic):
-
<ErrorBoundary>
Component:- Pros: Declarative, easy to place in JSX, good for wrapping sections of UI,
resetKeys
is very convenient. - Cons: Doesn't directly help with errors outside React's rendering (e.g., in async callbacks) unless those errors are then used to set state that causes a render error.
- Best for: General UI protection, wrapping routes, widgets, or any component tree where errors might occur during rendering.
- Pros: Declarative, easy to place in JSX, good for wrapping sections of UI,
-
useErrorHandler()
Hook:- Pros: Allows programmatic error throwing to an Error Boundary from anywhere in a functional component (event handlers,
useEffect
, async callbacks). Integrates error handling for non-render-cycle errors into the Error Boundary system. - Cons: Requires an
<ErrorBoundary>
component to be an ancestor in the tree to catch the thrown error. - Best for: Handling errors from async operations (like data fetching), complex event handlers, or custom hook logic where you want a UI fallback rather than just console logging.
- Pros: Allows programmatic error throwing to an Error Boundary from anywhere in a functional component (event handlers,
-
withErrorBoundary
HOC:- Pros: Can be a concise way to wrap components, especially if you have many instances that need the same error handling configuration. Can be useful for class components if you prefer HOCs.
- Cons: HOCs are a slightly older pattern; Hooks are generally preferred for new code. Resetting can be less direct than with
resetKeys
on the component. - Best for: Applying consistent error handling to multiple components, or when working in codebases that already use HOCs extensively.
General Recommendation:
- Start by strategically placing
<ErrorBoundary>
components around major UI sections, routes, and risky components. - Use
useErrorHandler()
inside functional components to propagate errors from async logic or event handlers to those boundaries when a UI fallback is desired. - Consider
withErrorBoundary
if it fits an existing HOC pattern in your project or if you need to protect many class components uniformly.
Often, you'll use a combination: <ErrorBoundary>
for declarative wrapping and useErrorHandler
for imperative error escalation.
✨ Section 5: Best Practices for Error Handling in a Hooks World
- Use
react-error-boundary
: It simplifies many aspects compared to rolling your own full-featured class component, especially the reset logic and Hooks integration. - Local
try...catch
for Local Recovery: For errors in event handlers or async code where you can recover locally (e.g., show a small toast notification, retry an operation silently), use standardtry...catch
or Promise.catch()
. Don't escalate every error to an Error Boundary if a local fix is better UX. - Escalate with
useErrorHandler
for UI Fallbacks: If an error in async code or an event handler means a significant part of the UI cannot function correctly, useuseErrorHandler
to trigger the nearest Error Boundary's fallback. - Combine with Data Fetching Libraries: Libraries like React Query or SWR have their own error handling mechanisms. You can often integrate these with
useErrorHandler
if you want their errors to also be caught by your UI Error Boundaries.// const { data, error } = useQuery(...);
// useErrorHandler(error); // If 'error' is set by useQuery, throw it to boundary - Clear
onError
Logging: Ensure youronError
prop on<ErrorBoundary>
(or incomponentDidCatch
) logs sufficient context to your reporting service. - User-Friendly Fallbacks: Always design fallbacks from the user's perspective. What do they need to see? Can they try again?
💡 Conclusion & Key Takeaways (Part 2)
react-error-boundary
significantly enhances our ability to manage errors gracefully in functional components. The useErrorHandler
hook is a powerful tool for programmatically triggering error boundaries from outside the rendering lifecycle, ensuring that errors from asynchronous operations or complex event handlers can still result in a user-friendly fallback UI. Combined with the declarative <ErrorBoundary>
component and the withErrorBoundary
HOC, you have a comprehensive toolkit for building resilient React applications.
Key Takeaways:
useErrorHandler
allows functional components to send errors from async code or event handlers to an Error Boundary.withErrorBoundary
HOC offers another way to wrap components for error protection.- Choose the approach (
<ErrorBoundary>
,useErrorHandler
,withErrorBoundary
) that best fits the specific error handling scenario. - Always prioritize clear, actionable fallback UIs and robust error logging.
➡️ Next Steps
We've now thoroughly covered creating, placing, and using Error Boundaries with both class and functional components, including leveraging the react-error-boundary
library. The next crucial step in a production application is ensuring these errors are not just caught but also reported. The next article, "Reporting Errors to a Service (Part 1)", will focus on how to integrate external error reporting services like Sentry into your Error Boundary setup.
Keep your UIs stable and your users happy!
glossary
useErrorHandler
: A hook fromreact-error-boundary
that returns a function; calling this function with an error will propagate it to the nearest Error Boundary.- Programmatic Error Handling: Triggering error handling logic explicitly through code (e.g., calling
useErrorHandler(error)
) rather than relying solely on errors thrown during rendering. - Higher-Order Component (HOC): A function that takes a component and returns a new component, often used to share component logic or enhance components.
withErrorBoundary
is an example. - Asynchronous Operation: Code that doesn't execute immediately or sequentially, such as data fetching,
setTimeout
, or Promise resolutions.
Further Reading
react-error-boundary
GitHub Documentation (Especially the API foruseErrorHandler
andwithErrorBoundary
).- React Docs: Error Handling in Event Handlers (Conceptual) (Explains event handlers don't use React's render lifecycle error path).
- Handling Errors in React with Error Boundaries (includes
useErrorHandler
examples)