Error Boundaries in React with TypeScript: Going Beyond the Basics



This content originally appeared on DEV Community and was authored by Harpreet Singh

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;
  }
}

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>
  );
}

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>;

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>

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.


This content originally appeared on DEV Community and was authored by Harpreet Singh