JavaScript and React course Β· Module 16: Error Handling and Suspense
Error Boundaries - The Guardians of Your Application
In this lesson5
In the previous lesson you saw how one faulty module shuts down the whole station, and how try/catch around JSX saves nothing. We need a mechanism that works where React actually renders the children. Error Boundaries are special React components that act like safety systems on a space station. When one module fails, the Error Boundary isolates the problem and prevents the destruction of the entire station, just like a bulkhead seals off a breached compartment.
What Are Error Boundaries?
An Error Boundary is a React class component that catches JavaScript errors anywhere in its child component tree, logs them, and displays a fallback UI instead of the tree that crashed. Why a class? The react.dev documentation is clear: there is currently no way to write an Error Boundary as a function component, because there is no hook equivalent to the methods below. Fortunately, you write such a component once and use it across the whole fleet.
Two Key Methods
An Error Boundary is a class component that implements one or both of these methods:
static getDerivedStateFromError(error)- Updates state so the next render shows the fallback UIcomponentDidCatch(error, errorInfo)- Logs error information (e.g., to a monitoring service)
The order matters. When a child throws, React first calls getDerivedStateFromError during the render phase, then calls render(), which already shows the fallback, and only after the changes are committed to the screen does it call componentDidCatch. That's why the first method must be pure (it only returns new state), while side effects, such as sending a report, belong in the second one.
Basic Implementation
Below is a complete, minimal bulkhead. The hasError state tells whether the compartment is breached, and error stores the error itself so we can show its message:
1import React from 'react';
2
3class ErrorBoundary extends React.Component {
4 constructor(props) {
5 super(props);
6 this.state = { hasError: false, error: null };
7 }
8
9 // Called when a child component throws an error
10 // Returns an object that updates the state
11 static getDerivedStateFromError(error) {
12 return { hasError: true, error: error };
13 }
14
15 // Called after an error has been caught
16 // Great place for logging errors
17 componentDidCatch(error, errorInfo) {
18 console.error('Error caught:', error);
19 console.error('Component stack:', errorInfo.componentStack);
20 }
21
22 render() {
23 if (this.state.hasError) {
24 return (
25 <div className="error-fallback">
26 <h2>Something went wrong!</h2>
27 <p>{this.state.error?.message}</p>
28 </div>
29 );
30 }
31
32 return this.props.children;
33 }
34}As long as nothing breaks, render() simply returns this.props.children, so the boundary is invisible and doesn't change how the children look or behave. The errorInfo.componentStack field is the path of components leading to the failure, invaluable when hunting for the cause.
Using Error Boundaries
An Error Boundary wraps the components we want to protect. Each station module gets its own bulkhead:
1function SpaceStation() {
2 return (
3 <div className="station">
4 <ErrorBoundary>
5 <Navigation />
6 </ErrorBoundary>
7
8 <ErrorBoundary>
9 <MainPanel />
10 </ErrorBoundary>
11
12 <ErrorBoundary>
13 <StatusBar />
14 </ErrorBoundary>
15 </div>
16 );
17}Now if MainPanel throws an error, Navigation and StatusBar will continue to work! A boundary only catches errors from its own subtree, so neighboring modules never even learn about the failure.
Error Boundary Granularity
You can use different levels of granularity. It helps to think of them as layers of a ship's shielding.
Application Level (General Safety Net)
The widest boundary wraps the whole application and catches everything the narrower ones missed:
1function App() {
2 return (
3 <ErrorBoundary fallback={<h1>Entire station failure!</h1>}>
4 <SpaceStation />
5 </ErrorBoundary>
6 );
7}This boundary won't protect neighbors, because it replaces everything with the fallback, but it turns a white screen into a readable message. We'll handle the fallback prop in our class in a moment.
Section Level (Module Isolation)
Narrower boundaries wrap larger sections, each with its own message:
1function SpaceStation() {
2 return (
3 <div>
4 <ErrorBoundary fallback={<p>Navigation offline</p>}>
5 <Navigation />
6 </ErrorBoundary>
7 <ErrorBoundary fallback={<p>Control panel unavailable</p>}>
8 <ControlPanel />
9 </ErrorBoundary>
10 </div>
11 );
12}A control panel failure shuts down only its own compartment, while navigation keeps steering the ship.
Component Level (Precise Isolation)
The finest-grained boundary protects individual widgets, for example every tile on the dashboard:
1function Dashboard() {
2 return (
3 <div className="dashboard">
4 {widgets.map(widget => (
5 <ErrorBoundary key={widget.id} fallback={<WidgetError />}>
6 <Widget data={widget} />
7 </ErrorBoundary>
8 ))}
9 </div>
10 );
11}Notice that the key goes on ErrorBoundary, because it is now the direct list element. My advice: start with an application-level boundary and add narrower ones where failure is likely, for example around data from external APIs.
What Error Boundaries Do NOT Catch
It's important to know that Error Boundaries do not catch errors in:
- Event handlers - use
try/catch - Asynchronous code -
setTimeout,fetch, Promises - Server-side rendering (SSR)
- Errors in the Error Boundary itself (rather than its children)
React 19 added an exception to point 2: errors thrown inside the function passed to startTransition from the useTransition hook reach the nearest boundary. In a regular handler you still need try/catch:
1// Event handler - requires try/catch
2function Button() {
3 const handleClick = () => {
4 try {
5 riskyOperation();
6 } catch (err) {
7 // Handle the error here
8 }
9 };
10 return <button onClick={handleClick}>Click</button>;
11}A boundary above this button won't react to an error from handleClick, because React isn't rendering anything at the moment of the click. Handling it is the handler's job.
Custom Fallback via Prop
Best practice is to create a reusable Error Boundary with a configurable fallback. This version checks whether the parent passed a fallback prop, and only if it's missing does it show a default message:
1class ErrorBoundary extends React.Component {
2 constructor(props) {
3 super(props);
4 this.state = { hasError: false, error: null };
5 }
6
7 static getDerivedStateFromError(error) {
8 return { hasError: true, error };
9 }
10
11 componentDidCatch(error, info) {
12 console.error('Error boundary caught:', error, info);
13 }
14
15 render() {
16 if (this.state.hasError) {
17 // Use the fallback prop if available
18 if (this.props.fallback) {
19 return this.props.fallback;
20 }
21 return <h2>An error occurred</h2>;
22 }
23 return this.props.children;
24 }
25}
26
27// Usage with a custom fallback
28<ErrorBoundary fallback={<p>Module damaged, but the station works!</p>}>
29 <RiskyModule />
30</ErrorBoundary>The catching logic didn't change, only what the crew sees did. In the next lesson you'll meet the react-error-boundary library, which adds reset support, automatic clearing and a hook for asynchronous errors on top of such a class.
Remember: an Error Boundary is an emergency bulkhead that seals one compartment so the rest of the ship can keep flying.
Code for this lesson: App.jsx
1import React, { useState } from 'react';
2import './styles.css';
3
4// Error Boundary - class component
5class ErrorBoundary extends React.Component {
6 constructor(props) {
7 super(props);
8 this.state = { hasError: false, error: null };
9 }
10
11 static getDerivedStateFromError(error) {
12 return { hasError: true, error };
13 }
14
15 componentDidCatch(error, errorInfo) {
16 console.error('ErrorBoundary caught:', error);
17 console.error('Component stack:', errorInfo.componentStack);
18 }
19
20 render() {
21 if (this.state.hasError) {
22 return (
23 <div className="error-fallback">
24 <h3>Module damaged</h3>
25 <p>{this.state.error?.message}</p>
26 <button
27 onClick={() => this.setState({ hasError: false })}
28 className="retry-btn"
29 >
30 Restart module
31 </button>
32 </div>
33 );
34 }
35 return this.props.children;
36 }
37}
38
39// Component that can throw an error
40function UnstableModule({ name, failRate = 0.5 }) {
41 if (Math.random() < failRate) {
42 throw new Error(name + ' - system failure!');
43 }
44 return (
45 <div className="module-ok">
46 <span className="status-dot green" />
47 {name}: Operational
48 </div>
49 );
50}
51
52// Stable component
53function StableModule({ name }) {
54 return (
55 <div className="module-ok">
56 <span className="status-dot green" />
57 {name}: Operational
58 </div>
59 );
60}
61
62export default function App() {
63 const [key, setKey] = useState(0);
64
65 return (
66 <div className="app">
67 <h1>Error Boundaries</h1>
68 <p className="subtitle">
69 Each module is protected by its own Error Boundary
70 </p>
71 <button
72 onClick={() => setKey(k => k + 1)}
73 className="reset-btn"
74 >
75 Reset station (key: {key})
76 </button>
77
78 <div className="station" key={key}>
79 <ErrorBoundary>
80 <UnstableModule name="Main engine" failRate={0.4} />
81 </ErrorBoundary>
82
83 <ErrorBoundary>
84 <UnstableModule name="Communication" failRate={0.4} />
85 </ErrorBoundary>
86
87 <ErrorBoundary>
88 <StableModule name="Life support systems" />
89 </ErrorBoundary>
90
91 <ErrorBoundary>
92 <UnstableModule name="Sensors" failRate={0.4} />
93 </ErrorBoundary>
94 </div>
95 </div>
96 );
97}Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What does the static getDerivedStateFromError(error) method do in an Error Boundary?
2. Which errors does an Error Boundary NOT catch?
Hands-on tasks in the game
- Click in order
Click in order the stages of Error Boundary operation after an error occurs:
- Code editor
Implement an Error Boundary
- Click in order
Click in order the elements needed to create an Error Boundary:
- Code editor
Implement a custom Error Boundary
- Vertical ordering
Order the levels of Error Boundaries granularity from broadest to narrowest: