Utilizing React ErrorBoundary

CNHur HyeonBin (Max)
Reponses  03months ago( Both Korean and English Version )

1. Introduction

When developing web applications with React, everyone has probably encountered a situation where a perfectly functioning application suddenly shows only a white screen or stops rendering anything with the console filled with red error messages. Modern web applications have numerous components organically and complexly interconnected, and without proper error handling, an error in just one component can cause the entire project to stop working. 

However, in such complex structures, just because one component has an error doesn't mean the entire application should stop, and users should be able to use other functions normally except for the specific area where the problem occurred.

ErrorBoundary, introduced in React 16, is a component that helps other components function normally by performing error handling independently only in the relevant part when an error occurs in a specific component or part of the project. It isolates errors that occur in specific parts of the component tree, providing appropriate fallback UI to users without affecting other areas, greatly helping to improve UX.

 


2. Purpose of Error Boundary

complecated component diagram.png

When components are structured in a tree form as shown in the diagram above, Error Boundary allows you to create strategic error breakpoints. For example, when an error occurs in Component G, developers can design whether to handle it within that component itself, at the Component E level above, or propagate it to a higher level.

In actual development, various types of errors occur, such as Authentication Error, Network Error, TypeError, etc. By properly utilizing Error Boundary, you can respond differently according to error types even within a single boundary, enabling more detailed and user-friendly error handling.
React projects are implemented in a tree form with react elements as nodes, connecting multiple components. Just because one component breaks doesn't mean the entire tree should stop, and showing UI appropriate for the tree that needs to handle the error is the correct error handling approach in React.

To summarize briefly in one line, React creates a DOM in tree form by connecting multiple components and renders it on screen. Just because an error occurs in one component doesn't mean the entire project should stop working, and the ability to cut out only the minimum tree where the error occurred and handle the error using error boundary is the biggest reason for using error boundary.

3. Utilizing Error Boundary

Let's assume there's a simple application. When you select a Category and then select a Sub Category, it's a simple application that shows the correct item list matching that category and subcategory.

 

sample application iamge.png

 

Looking at this in code from App.tsx, it would be like this.

function App() {
 return (
   <div>
     <CategoryList />
     <SubcategoryList />
     <ItemList />
   </div>
 );
}

 

Also, each List performs additional logic such as communicating with APIs using Custom Hooks and renders list items through Cards.

const ItemList = () => {
 const selectedSubcategoryId = useAppSelector(
   (state) => state.selection.selectedSubcategoryId
 );
 const { filteredItems } = useItems(selectedSubcategoryId);
 
 return (
   <div>
     <h3>Items</h3>
     <div>
       {filteredItems.map((item) => (
         <ItemCard key={item.id} />
       ))}
     </div>
   </div>
 );
};

 

This pattern of App, List, Card is very commonly used when showing List Items with React, and if displayed as a tree-form diagram, it would look like this.

sample application component structure diagram.png

Like this, when multiple components have child components and child components are nested deeply from the root component, Error Boundary helps manage various errors by tree units easily. Let me give you a specific example. If ItemList attempts an API request but it doesn't work properly and an error occurs, without proper error handling, in production mode, a white screen with red error messages in the console is displayed, and in development mode, the project stops working and errors are displayed on screen in red text.

However, using Error Boundary completely changes the situation. When an error occurs in ItemList, only components related to ItemList become in error state, and CategoryList and SubCategoryList, which are not directly related to ItemList, continue to function normally.

The ErrorBoundary provided by React official documentation is as follows. 

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {    
     return { hasError: true };  
	}
	
  componentDidCatch(error, errorInfo) {    
	logErrorToMyService(error, errorInfo);  
   }
  render() {
    if (this.state.hasError) {     
        return <h1>Something went wrong.</h1>;    
    }
    return this.props.children;
  }
}

 

Through getDerivedStateFromError(), errors thrown in the render phase are detected to show fallback UI through state changes, and through componentDidCatch(), side effects are handled in the commit phase when errors are detected. You can define and use error boundary by defining one (or both) of these.

Using basic error boundary like this, the parent can detect errors thrown by child components through getDerivedStateFromError() in the render phase, and render fallbackUI through render(), enabling error handling within specific trees rather than stopping the entire tree's operation.

However, while working on projects, you encounter various situations like Auth Error, Network Error, TypeError, Server Error, etc. In such situations, error boundary can respond flexibly.

When throwing predicted errors in projects, I often create CustomError instead of throwing general errors provided by JS. 

export default class CustomError extends Error {
 public readonly code: string;
 public readonly statusCode?: number;
 
 constructor(message: string, code: string, statusCode?: number) {
   super(message);
   this.name = "CustomError";
   this.code = code;
   this.statusCode = statusCode;
 }
}


As in the code above, define CustomError and throw the defined CustomError in predicted situations. In this situation, I tried throwing an error from ItemCard.

export default function ItemCard({ id, name }: ItemComponentProps) {
 if (error occurs when failing to successfully fetch item) {
   throw new CustomError(
     "Failed to fetch item data from server",
     "ITEMS_FETCH_ERROR",
     500
   );
 }
 
 if (network error occurs) {
   throw new CustomError(
     "Failed to fetch item data from server",
     "ITEMS_FETCH_ERROR",
     500
   );
 }
 
 if (access permission error occurs) {
   throw new CustomError(
     "You don't have permission to access this item",
     "PERMISSION_DENIED",
     403
   );
 }
 
 if (unexpected error occurs) {
   throw new CustomError("Item data not found", "ITEM_NOT_FOUND", 404);
 }
 
 return (
   <div>
     <div>
       <span>#{id}</span>
       <span>{name}</span>
     </div>
   </div>
 );
}

 

Like this, by newly defining and throwing predictable errors through CustomError, Errorboundary can determine what error currently occurred and what fallbackUI should be shown through CustomError's name, code, statusCode, and respond to various errors.

export default class ItemErrorBoundary extends React.Component
 { children: React.ReactNode },
 ErrorBoundaryState
> {
 constructor(props: { children: React.ReactNode }) {
   super(props);
   this.state = {
     hasError: false,
     isNetworkError: false,
     isPermissionError: false,
     isNotFoundError: false,
     isGeneralError: false,
     errorMessage: "",
   };
 }

 static getDerivedStateFromError(error: Error): ErrorBoundaryState {
   // Base state
   const baseState: ErrorBoundaryState = {
     hasError: true,
     error,
     isNetworkError: false,
     isPermissionError: false,
     isNotFoundError: false,
     isGeneralError: false,
     errorMessage: error.message,
   };

   // Check if it's CustomError and set flags according to error code
   if (isCustomError(error)) {
     baseState.errorCode = error.code;
     baseState.statusCode = error.statusCode;
     switch (error.code) {
       case "ITEMS_FETCH_ERROR":
         baseState.isNetworkError = true;
         console.log("Setting isNetworkError flag");
         break;
       case "PERMISSION_DENIED":
         baseState.isPermissionError = true;
         console.log("Setting isPermissionError flag");
         break;
       case "ITEM_NOT_FOUND":
         baseState.isNotFoundError = true;
         console.log("Setting isNotFoundError flag");
         break;
       default:
         baseState.isGeneralError = true;
         console.log("Setting isGeneralError flag for unknown custom error");
     }
   } else {
     baseState.isGeneralError = true;
   }

   return baseState;
 }

 render() {
   if (this.state.hasError) {
     // Render different UI according to flags
     if (this.state.isNetworkError) {
       return <NetworkError/>;
     }
     if (this.state.isPermissionError) {
       return <PermissionError/>;
     }
     if (this.state.isNotFoundError) {
       return <NotFoundError/>;
     }
     if (this.state.isGeneralError) {
       return <GeneralError/>;
     }
   }
   return this.props.children;
 }
}

 

 By defining and responding to multiple error types as above, and using them divided into small desired trees, the tree and displayed UI for that error handling are as follows. If you've properly created Errorboundary, all application is complete just by wrapping the part that renders ItemCard.

const ItemList = () => {
 const selectedSubcategoryId = useAppSelector(
   (state) => state.selection.selectedSubcategoryId
 );
 const { filteredItems, error } = useItems(selectedSubcategoryId);
 
 return (
   <div>
     <h3>Items</h3>
     <div>
       {filteredItems.map((item) => (
         <ItemErrorBoundary key={item.id}>
           <ItemCard id={item.id} name={item.name} />
         </ItemErrorBoundary>
       ))}
     </div>
   </div>
 );
};

Errorboundary Flow Diagram.png

Even when multiple ItemCards exist and each ItemCard throws different errors, Error Boundary enables the entire project to function normally while properly handling each error.

 

Handled Error Example.png

The continuously mentioned tree-unit error handling means that even when errors occur, instead of the entire project stopping, internal small tree units handle errors so there are no problems with overall operation. Through ErrorBoundary, additional responses to errors are possible, such as re-rendering components, refreshing entire pages, or navigating to previous pages.

Also, if an error cannot be handled in the current tree, applications such as passing the error to upper error boundary to handle the error in higher parent trees are possible.

In the current code, ErrorBoundary is defined and used based on specific trees, but if you create error boundaries by function or error type rather than tree and component-based and apply them to projects, the reusability of error boundaries can be greatly improved.

For example, creating AuthErrorBoundary to wrap specific routes to detect Auth Errors within specific routes, or creating Errorboundary that manages only NetworkError to handle errors that occur redundantly in multiple trees is also one of the common patterns of error boundary.

function App() {
 return (
   <div>
     <h1 className="app-title">3-Level Hierarchical List System</h1>
     <CategoryList />
     <AuthErrorBoundary> 
       <SubcategoryList />
     </AuthErrorBoundary>
     <AuthErrorBoundary>
       <ItemList />
     </AuthErrorBoundary>
   </div>
 );
}

 


Conclusion

Error Boundary presented in React 16 is a Component appropriate for handling errors on the Frontend side in line with React's concept. As covered in this post, there are very diverse directions for application, and it shows excellent aspects in UX improvement by providing fallback UI that allows users to respond to errors within the project or directly when users encounter errors.

However, Error Boundary is not omnipotent. There are clear limitations that it cannot detect errors occurring in event handlers, asynchronous code, setTimeout callbacks, etc. Fortunately, the React and Next.js ecosystem presents various patterns to complement these limitations, such as the `react-error-boundary` library, error delivery through custom hooks, and combinations with Suspense.

If you've applied Error Boundary in your project but it doesn't work as expected, please first check if the error occurred within React's rendering cycle. Understanding these constraints and utilizing appropriate alternatives together will enable you to build more robust and user-friendly applications.

CNHur HyeonBin (Max)
Reponses  0