Skip to main content

Creating a Reusable Error Boundary Component (Part 1) #138

📖 Introduction

Having understood What are Error Boundaries? and their importance in creating resilient React applications, it's time to get practical. In this article, we'll begin the process of building a reusable Error Boundary class component from scratch. This component will serve as a foundation that you can adapt and use across various parts of your projects to catch and handle UI errors gracefully. Part 1 will focus on the basic structure, state management, and rendering the fallback UI.


📚 Prerequisites

Before we begin, ensure you have:

  • A solid grasp of Error Boundary concepts (Article 137).
  • Experience with React class components, state, and props.
  • Knowledge of the static getDerivedStateFromError() and componentDidCatch() lifecycle methods.

🎯 Article Outline: What You'll Master

In this article, you will learn:

  • Designing the ErrorBoundary Props: What configurability do we need? (e.g., custom fallback UI).
  • Initial Class Component Structure: Setting up the constructor and initial state.
  • Implementing static getDerivedStateFromError(): Updating state when an error is caught.
  • Basic Fallback UI Rendering: Conditionally rendering children or a fallback based on error state.
  • Testing the Basic Error Boundary: Creating a component that intentionally throws an error to see the boundary in action.

🧠 Section 1: Designing Our Reusable Error Boundary

Before we write code, let's think about what makes an Error Boundary "reusable." A good reusable component often allows for some level of customization via props. For our ErrorBoundary, key considerations are:

  1. Fallback UI: The most crucial part. Do we hardcode a fallback, or allow the parent component to provide a custom fallback UI via props? Allowing a custom fallback offers maximum flexibility. We could also provide a default fallback if none is given.
  2. Error Logging: While componentDidCatch will handle logging, should the component accept callbacks for custom logging or actions? (We'll delve more into this in Part 2).
  3. Resetting State: Sometimes, after an error, we might want to provide a way for the user to "try again," which might involve resetting the error boundary's state. (Also a topic for Part 2).

For Part 1, we'll focus primarily on a flexible fallback UI.

Props Plan:

  • children: Standard React prop for the components to be wrapped and protected.
  • fallback: An optional React element to be rendered if an error occurs. If not provided, we'll render a generic default message.
  • fallbackComponent: An alternative to fallback, allowing a component to be rendered (e.g., fallbackComponent={MyCustomErrorFallback}). This can be more powerful if the fallback needs its own logic or props.

Let's start by supporting a simple fallback prop (React element) and a default message.


💻 Section 2: Basic Structure and State

Let's create ErrorBoundary.jsx:

// src/components/ErrorBoundary.jsx
import React, { Component } from 'react';

class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = {
hasError: false, // True if an error is caught in a child
error: null, // The actual error object
errorInfo: null // Component stack information
};
}

// Lifecycle methods will go here

render() {
if (this.state.hasError) {
// If a fallback prop is provided, render it
if (this.props.fallback) {
return this.props.fallback;
}
// Otherwise, render a default fallback UI
return (
<div style={{ padding: '20px', border: '1px solid red', margin: '10px' }}>
<h2>Oops! Something went wrong.</h2>
<p>We're sorry, an unexpected error occurred in this section of the application.</p>
{/* In a real app, you might not want to show error details directly in production UI */}
{process.env.NODE_ENV === 'development' && this.state.error && (
<details style={{ whiteSpace: 'pre-wrap', marginTop: '10px' }}>
<summary>{this.state.error.toString()}</summary>
{this.state.errorInfo && this.state.errorInfo.componentStack}
</details>
)}
</div>
);
}

// Normally, just render children
return this.props.children;
}
}

export default ErrorBoundary;

Explanation:

  • Constructor:
    • Initializes state with hasError: false (no error initially).
    • error and errorInfo are initialized to null to store details when an error occurs.
  • render() Method:
    • Checks this.state.hasError.
    • If true:
      • It first checks if a this.props.fallback (a React element) was passed. If so, it renders that custom fallback.
      • If no fallback prop is provided, it renders a default error message.
      • In development mode (process.env.NODE_ENV === 'development'), it also includes a <details> section to show the error message and component stack for easier debugging. You would typically remove or hide this detailed info in a production build for security and UX reasons.
    • If false (no error), it renders this.props.children as usual.

This structure gives us a basic error boundary that can either show a default message or a custom one passed via props.


🛠️ Section 3: Implementing static getDerivedStateFromError()

This lifecycle method is crucial for updating the state when an error is caught, which in turn triggers the rendering of the fallback UI.

// src/components/ErrorBoundary.jsx
import React, { Component } from 'react';

class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null
};
}

// --- ADD THIS METHOD ---
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
// It should return a state update object.
console.log('ErrorBoundary: getDerivedStateFromError caught an error:', error);
return { hasError: true, error: error }; // We also store the error itself
}
// --- END OF ADDED METHOD ---

// componentDidCatch will be added in the next section or Part 2

render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div style={{ padding: '20px', border: '1px solid red', margin: '10px' }}>
<h2>Oops! Something went wrong.</h2>
<p>We're sorry, an unexpected error occurred in this section of the application.</p>
{process.env.NODE_ENV === 'development' && this.state.error && (
<details style={{ whiteSpace: 'pre-wrap', marginTop: '10px' }}>
<summary>{this.state.error.toString()}</summary>
{/* errorInfo will be populated by componentDidCatch */}
{this.state.errorInfo ? this.state.errorInfo.componentStack : 'Component stack not available yet.'}
</details>
)}
</div>
);
}
return this.props.children;
}
}

export default ErrorBoundary;

Changes:

  • We added the static getDerivedStateFromError(error) method.
  • When an error occurs in a child, this method is called by React.
  • It returns { hasError: true, error: error }. This updates the ErrorBoundary's state:
    • hasError becomes true, causing the fallback UI to render.
    • error now stores the actual error object, which we use in our development-only details display.
  • The console.log is for demonstration; in a production component, you might remove it from here as componentDidCatch is better for logging side-effects.

At this point, our ErrorBoundary can catch an error and switch to displaying a fallback UI. It doesn't yet log the componentStack (that's for componentDidCatch), but it's functional for UI swapping.


🔬 Section 4: Testing the Basic Error Boundary

To test our ErrorBoundary, we need a component that intentionally throws an error.

1. Create a "Problematic" Component:

// src/components/ProblematicComponent.jsx
import React, { useState } from 'react';

const ProblematicComponent = () => {
const [throwError, setThrowError] = useState(false);

if (throwError) {
throw new Error('This is a test error from ProblematicComponent!');
}

return (
<div>
<p>I am usually a well-behaved component.</p>
<button onClick={() => setThrowError(true)}>
Click me to throw an error
</button>
</div>
);
};

export default ProblematicComponent;

This component renders normally until the button is clicked, at which point it throws an error during its re-render.

2. Use ErrorBoundary in your App:

// src/App.js (or any component where you want to test)
import React from 'react';
import ErrorBoundary from './components/ErrorBoundary';
import ProblematicComponent from './components/ProblematicComponent';
import './App.css'; // Basic styling

function App() {
const customFallbackUI = (
<div style={{ backgroundColor: 'lightyellow', border: '1px solid orange', padding: '15px', margin: '10px' }}>
<h3>A Custom Fallback!</h3>
<p>Something definitely went haywire in the child component.</p>
<p>But don't worry, the rest of the app is still fine!</p>
</div>
);

return (
<div className="App">
<header className="App-header">
<h1>React Error Boundary Test</h1>
</header>
<main>
<section>
<h2>Section 1: No Error Boundary</h2>
<p>This problematic component is NOT wrapped. Clicking its button will likely crash the app (or show React's default dev overlay).</p>
{/* <ProblematicComponent /> */}
{/* Uncomment above line carefully - it might crash your dev server's auto-refresh client */}
<p><em>(ProblematicComponent commented out here to prevent app crash during demo)</em></p>
</section>

<hr />

<section>
<h2>Section 2: With Default Error Boundary Fallback</h2>
<ErrorBoundary>
<p>This component is inside an ErrorBoundary with a default fallback:</p>
<ProblematicComponent />
</ErrorBoundary>
</section>

<hr />

<section>
<h2>Section 3: With Custom Error Boundary Fallback</h2>
<ErrorBoundary fallback={customFallbackUI}>
<p>This component is inside an ErrorBoundary with a <strong>custom</strong> fallback UI:</p>
<ProblematicComponent />
</ErrorBoundary>
</section>

<hr />

<section>
<h2>Section 4: Healthy Content</h2>
<ErrorBoundary>
<p>This content is also wrapped in an ErrorBoundary but should render normally:</p>
<div>This is a healthy component and should always be visible.</div>
</ErrorBoundary>
</section>
</main>
</div>
);
}

export default App;

Testing Steps:

  1. Run your React application (npm start or yarn start).
  2. Observe Section 2:
    • Click the "Click me to throw an error" button within "Section 2".
    • You should see the ErrorBoundary's default fallback UI appear, replacing the ProblematicComponent. The rest of the app (headings, other sections) should remain visible and functional.
    • Check your browser's console. You should see the log from getDerivedStateFromError.
  3. Observe Section 3:
    • Click the button within "Section 3".
    • You should see your customFallbackUI rendered by the ErrorBoundary.
  4. Observe Section 4:
    • This section should render normally as its child component doesn't throw an error.

If you uncomment the ProblematicComponent in "Section 1" (and your development environment doesn't have its own global error overlay that masks React's behavior), you'd typically see the whole app crash or become unresponsive, demonstrating the need for error boundaries.


💡 Conclusion & Key Takeaways (Part 1)

We've successfully laid the foundation for a reusable ErrorBoundary component. By implementing the constructor and static getDerivedStateFromError, our component can now catch errors from its children and render either a default or a custom fallback UI. This is the core mechanism for preventing UI crashes and providing a better user experience when things go wrong.

Key Takeaways So Far:

  • A reusable ErrorBoundary should manage hasError and error in its state.
  • static getDerivedStateFromError(error) is the lifecycle method responsible for updating state to trigger a fallback UI when a child throws an error.
  • The render method conditionally displays either props.children or a fallback UI based on the hasError state.
  • Providing a fallback prop allows for flexible, custom error displays.

Next Steps: While our ErrorBoundary can now show a fallback, it's not yet logging detailed error information (like the component stack) or offering a way to reset the error. These crucial features will be the focus of Part 2.


➡️ Next Steps

In "Creating a Reusable Error Boundary Component (Part 2)", we will enhance our ErrorBoundary by:

  • Implementing componentDidCatch() for robust error logging (including component stack traces).
  • Adding functionality to allow users to "try again" by resetting the error state.
  • Discussing further customization options and best practices.

Stay tuned to complete our journey in building a production-ready Error Boundary!


glossary

  • Reusable Component: A React component designed to be used in multiple places within an application, often configurable via props.
  • Fallback UI Prop (fallback): A prop passed to an Error Boundary that specifies a React element to render when an error is caught.
  • process.env.NODE_ENV: An environment variable commonly used in JavaScript projects (especially those built with Node.js and bundlers like Webpack) to indicate whether the code is running in a 'development', 'production', or 'test' environment.

Further Reading