DEV Community

Cover image for Error Boundaries in React with TypeScript: Going Beyond the Basics
Harpreet Singh
Harpreet Singh

Posted on

Error Boundaries in React with TypeScript: Going Beyond the Basics

We all know the classic React error boundary: wrap a component, catch rendering errors, and show a fallback UI. But if you’ve worked on real-world apps, you know the “textbook” approach often falls short. Async errors, route-specific crashes, and logging needs require a more advanced setup.

In this article, I’ll walk you through a TypeScript-first approach to error boundaries that makes your React apps more resilient, easier to debug, and more user-friendly.


1. The Strongly Typed Error Boundary

First, let’s define a solid, TypeScript-friendly error boundary that handles errors gracefully and logs them:

import React from "react"; interface ErrorBoundaryProps { children: React.ReactNode; fallback?: React.ReactNode; } interface ErrorBoundaryState { hasError: boolean; error?: Error; } export class AppErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> { state: ErrorBoundaryState = { hasError: false }; static getDerivedStateFromError(error: Error): ErrorBoundaryState { return { hasError: true, error }; } componentDidCatch(error: Error, info: React.ErrorInfo) { // Log to monitoring service console.error("Logged Error:", error, info); } render() { if (this.state.hasError) { return this.props.fallback ?? <h2>Something went wrong.</h2>;  } return this.props.children; } } 
Enter fullscreen mode Exit fullscreen mode

This gives you type safety for both props and state while keeping your error handling centralized.


2. Route-Level Boundaries: Isolate the Crashes

Instead of one giant boundary at the root of your app, wrap specific routes or features. This way, a single failing page doesn’t crash your whole app:

import { AppErrorBoundary } from "./AppErrorBoundary"; import Dashboard from "./Dashboard"; function DashboardRoute() { return ( <AppErrorBoundary fallback={<h2>Dashboard failed to load.</h2>}> <Dashboard /> </AppErrorBoundary> ); } 
Enter fullscreen mode Exit fullscreen mode

Users can still navigate your app, even if one page has an error.


3. Handling Async and Event Errors

React error boundaries don’t catch errors in async/await or event handlers. To fix this, wrap your async functions:

function safeAsync<T extends (...args: any[]) => Promise<any>>(fn: T) { return async (...args: Parameters<T>): Promise<ReturnType<T>> => { try { return await fn(...args); } catch (err) { console.error("Async error:", err); throw err; // optional: let ErrorBoundary catch it if needed } }; } // Usage const handleClick = safeAsync(async () => { throw new Error("Boom!"); }); <button onClick={handleClick}>Click Me</button>; 
Enter fullscreen mode Exit fullscreen mode

This ensures async crashes are logged and can be optionally caught by your boundary.


4. Resettable Boundaries: Let Users Recover

A frozen fallback UI is frustrating. With react-error-boundary, you can provide a retry button:

import { ErrorBoundary } from "react-error-boundary"; function Fallback({ error, resetErrorBoundary }: { error: Error; resetErrorBoundary: () => void; }) { return ( <div> <p>Something went wrong: {error.message}</p> <button onClick={resetErrorBoundary}>Try Again</button> </div> ); } <ErrorBoundary FallbackComponent={Fallback} onError={(error) => console.error("Caught by boundary:", error)} resetKeys={[/* state/props that trigger reset */]} > <Dashboard /> </ErrorBoundary> 
Enter fullscreen mode Exit fullscreen mode

Users can recover without having to refresh the page.


5. Layered Approach for Production-Ready Apps

Combine these strategies for a robust setup:

  • Global boundary - catches catastrophic failures.
  • Route/component boundaries - isolate crashes.
  • Async wrappers + logging - capture what React misses.
  • Resettable fallbacks - improve user experience.

This layered approach keeps your app resilient and your users happy.


Wrapping Up

React’s built-in error boundaries are just the starting point. In real apps, you need a TypeScript-first, layered strategy:

  • Strong typing for safety
  • Logging for observability
  • Isolation for reliability
  • Recovery for UX

This way, errors are no longer showstoppers, they’re just part of a manageable system.


If you enjoyed this, check out my other articles for more advanced, production-ready patterns.

Top comments (0)