While managing a project at work, I felt that the error-handling logic was too scattered, and when an error occurred inside a React component, I needed to structure the logic so that the overall error flow could be managed.
I want to summarize how to handle errors inside a React component, and how I ended up handling errors while using react-router-dom v6.
Error Boundaries
Starting from React 16, the concept of an Error Boundary was introduced. It provides two lifecycle methods so that errors can be caught in the lifecycle during rendering or in a subtree.
The official React documentation guides you to use static getDerivedStateFromError() to render a fallback UI after an error occurs, and componentDidCatch() to log error information.
class
jsxclass ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Update state so that the next render shows the fallback UI.
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// You can also log the error to an error reporting service.
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// You can render a custom fallback UI.
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
You create a class ErrorBoundary component with the error lifecycle methods defined as above, and wrap the project's App component with it so that errors occurring during rendering are caught.
jsx<ErrorBoundary>
<App />
</ErrorBoundary>
ErrorBoundary behaves similarly to a catch {} statement and is used as an error boundary within components. However, there are several kinds of errors that this error catching cannot capture.
- Event handlers
- Asynchronous code (setTimeout, requestAnimationFrame, Promise)
- Server-side rendering
- Errors thrown in the ErrorBoundary itself
functional
The official site only showed how to handle errors with the lifecycle methods of a class, and I wondered whether it wouldn't be possible to implement an ErrorBoundary as a functional component.
The reason I needed a functional ErrorBoundary component was that, when an error occurred, there were parts where I wanted to call the necessary hooks inside the ErrorBoundary to perform post-processing of the error.
One method I found through searching was to wrap a class-defined ErrorBoundary component once with a function to handle errors.
jsximport React from "react"
type ErrorHandler = (error: Error, info: React.ErrorInfo) => void
type ErrorHandlingComponent<Props> = (props: Props, error?: Error) => React.ReactNode
type ErrorState = { error?: Error }
export default function Catch<Props extends {}>(
component: ErrorHandlingComponent<Props>,
errorHandler?: ErrorHandler
): React.ComponentType<Props> {
function Inner(props: { error?: Error, props: Props }) {
return <React.Fragment>{component(props, error)}</React.Fragment>
}
return class extends React.Component<Props, ErrorState> {
state: ErrorState = {
error: undefined
}
static getDerivedStateFromError(error: Error) {
return { error }
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
if (errorHandler) {
errorHandler(error, info)
}
}
render() {
return <Inner error={this.state.error} props={this.props} />
}
}
}
Error Event
Another form: when I asked chatgpt to convert it into a functional component, it turned it into a fairly (?) decent form that leverages error events.
jsximport React, { useState, useEffect } from "react";
function ErrorBoundary({ children }) {
const [hasError, setHasError] = useState(false);
const [error, setError] = useState(null);
const [errorInfo, setErrorInfo] = useState(null);
useEffect(() => {
const componentDidCatch = (error, errorInfo) => {
setHasError(true);
setError(error);
setErrorInfo(errorInfo);
// You can also log the error to an error reporting service
// or perform any other necessary error handling here
};
// Register the componentDidCatch method as the error boundary
window.addEventListener("error", componentDidCatch);
// Clean up the event listener
return () => {
window.removeEventListener("error", componentDidCatch);
};
}, []);
const getDerivedStateFromError = (error) => {
// Update the state based on the error
return {
hasError: true,
error: error,
};
};
if (hasError) {
// Fallback UI when an error occurs
return (
<div>
<h1>Something went wrong.</h1>
<p>{error && error.toString()}</p>
<p>Component Stack Trace:</p>
<pre>{errorInfo && errorInfo.componentStack}</pre>
</div>
);
}
// Render the normal component tree when no error has occurred
return children;
}
ErrorBoundary.getDerivedStateFromError = getDerivedStateFromError;
export default ErrorBoundary;
The componentDidCatch part is handled by registering an error event, and getDerivedStateFromError is registered and processed as a static method.
Additionally, if you need to handle callback errors for asynchronous code (setTimeout, requestAnimationFrame, Promise, etc.), you can do extra error handling in the unhandledrejection event.
jsxconst errorHandler = (error: PromiseRejectionEvent) => {
setHasError(true);
setError(error);
};
window.addEventListener("unhandledrejection", errorHandler);
react-router-dom errorElement
In a previous project, I tried moving the parts where errors were handled only through events to setting an errorElement on the react-router-dom Route. The errorElement set on a Route internally wrapped routing with the ErrorBoundary that React guided, performing error handling.
The disappointing part was that internally it could not catch asynchronous errors, so I still had to add separate error handling for asynchronous callbacks, like the event handling described above.
Inside the error page, I was able to process error information using the useRouteError hook, which passes the error information for the errorElement. And through exception handling in the loader and action that can be set on a Route, I could configure the error information so that it could be received via useRouteError.
Through exception handling in the loader and action that can be set on the Route above, error handling for APIs or asynchronous processing is also possible, and the react-router-dom examples also included error-handling samples using this method. However, for handling things like API calls triggered by state or user interaction within a component, additional separate work was required.
In conclusion
The parts that provide the error component setup and hooks for routing were nice, but when additional error handling is needed for specific situations such as asynchronous error handling, the error-handling logic ends up scattered across multiple places, so I think a refactor to handle it all in one place will be necessary. 🤔
[Ref]: