React Error Handling in Functional Components

Managing errors in React has always been a bit of a puzzle, especially when moving from class components to functional ones.

I remember the first time I built a large-scale dashboard for a fintech firm in New York; a single failed API call crashed the entire user interface.

It was a frustrating experience, but it taught me that robust error handling is what separates a junior developer from a seasoned pro.

In this tutorial, I will show you how to effectively catch and manage errors in React functional components using real-world scenarios.

Why Error Handling Matters in React

In the early days of React, a JavaScript error inside a component would corrupt React’s internal state and cause it to emit cryptic errors on next renders.

React 16 introduced “Error Boundaries” to solve this, ensuring that a crash in one part of the UI doesn’t take down the whole app.

When you are building applications for high-stakes industries like healthcare or finance, you simply cannot afford a “white screen of death.”

Method 1: Use a Global Error Boundary

While we primarily use functional components today, Error Boundaries must still be defined as class components. This is a common point of confusion.

I always recommend wrapping your main layout or specific high-risk modules in an Error Boundary to provide a fallback UI.

In this example, imagine we are building a stock portfolio tracker for a brokerage based in Chicago.

import React, { Component } from 'react';

// This is the Error Boundary Class
class PortfolioErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // Update state so the next render shows the fallback UI.
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // You can also log the error to an error reporting service like Sentry
    console.error("Logging to monitoring service:", error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // Custom fallback UI for a US-based Stock App
      return (
        <div style={{ padding: '20px', textAlign: 'center', backgroundColor: '#fff5f5' }}>
          <h2>Something went wrong loading your Portfolio.</h2>
          <p>Please refresh the page or contact support at support@us-brokerage.com.</p>
        </div>
      );
    }

    return this.props.children; 
  }
}

// Functional Component that might crash
const StockList = ({ stocks }) => {
  if (!stocks) {
    throw new Error("No stock data available!");
  }

  return (
    <ul>
      {stocks.map(stock => (
        <li key={stock.ticker}>{stock.ticker}: ${stock.price}</li>
      ))}
    </ul>
  );
};

// Implementation
export default function App() {
  return (
    <div>
      <h1>Wall Street Insights</h1>
      <PortfolioErrorBoundary>
        <StockList stocks={null} />
      </PortfolioErrorBoundary>
    </div>
  );
}

You can see the output in the screenshot below.

React Error Handling in Functional Components

By using this method, the rest of your application remains interactive even if the StockList fails.

Method 2: Handle Logic Errors with Try-Catch and State

Error Boundaries are great for rendering errors, but they do not catch errors inside event handlers or asynchronous code like fetch calls.

I often use a combination of try-catch blocks and local state to manage these scenarios gracefully.

Let’s look at a component that calculates Sales Tax for a retail app serving customers in California and Texas.

import React, { useState } from 'react';

const TaxCalculator = () => {
  const [amount, setAmount] = useState('');
  const [tax, setTax] = useState(null);
  const [errorMessage, setErrorMessage] = useState('');

  const calculateSalesTax = (e) => {
    e.preventDefault();
    setErrorMessage('');
    
    try {
      const parsedAmount = parseFloat(amount);
      
      if (isNaN(parsedAmount) || parsedAmount <= 0) {
        throw new Error("Please enter a valid USD amount.");
      }

      // Logic for a flat 8% tax rate
      const calculatedTax = parsedAmount * 0.08;
      setTax(calculatedTax.toFixed(2));
      
    } catch (err) {
      setErrorMessage(err.message);
      setTax(null);
    }
  };

  return (
    <div style={{ border: '1px solid #ccc', padding: '20px', maxWidth: '400px' }}>
      <h3>US Retail Sales Tax Calculator</h3>
      <form onSubmit={calculateSalesTax}>
        <input 
          type="number" 
          value={amount} 
          onChange={(e) => setAmount(e.target.value)} 
          placeholder="Enter amount in USD"
        />
        <button type="submit">Calculate</button>
      </form>

      {errorMessage && (
        <p style={{ color: 'red', fontWeight: 'bold' }}>Error: {errorMessage}</p>
      )}

      {tax && (
        <p style={{ color: 'green' }}>Total Tax: ${tax}</p>
      )}
    </div>
  );
};

export default TaxCalculator;

You can see the output in the screenshot below.

Error Handling in Functional Components in React

This approach is highly effective because it gives the user immediate feedback without crashing the component.

Method 3: Handle Asynchronous Errors in useEffect

Catching errors in useEffect is a frequent requirement when dealing with US-based APIs, such as the Federal Reserve Economic Data (FRED).

Since useEffect cannot be an async function itself, you must handle the promise rejection inside the internal function.

Here is how I typically structure data fetching to ensure errors are caught before they reach the UI.

import React, { useState, useEffect } from 'react';

const RealEstateTrends = () => {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchMarketData = async () => {
      try {
        setLoading(true);
        // Simulating a fetch to a US Real Estate API
        const response = await fetch('https://api.example.com/us-housing-market');
        
        if (!response.ok) {
          throw new Error('Failed to fetch data from the housing server.');
        }

        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchMarketData();
  }, []);

  if (loading) return <p>Loading market data for San Francisco...</p>;
  if (error) return <p style={{ color: 'red' }}>Market Data Error: {error}</p>;

  return (
    <div>
      <h3>Current Real Estate Trends</h3>
      {data.map((city) => (
        <div key={city.id}>{city.name}: {city.trend}</div>
      ))}
    </div>
  );
};

export default RealEstateTrends;

You can see the output in the screenshot below.

Error Handling in Functional Components React

I find that using a loading and error state is the cleanest way to manage the user experience during network requests.

Method 4: Use the react-error-boundary Library

In my recent projects, I have started using the react-error-boundary library. It’s a lightweight wrapper that lets you use functional components for fallback UIs.

It is much more “React-like” and fits perfectly into a modern hooks-based architecture.

Imagine we are building a weather app for residents in Florida to track hurricane updates.

import React from 'react';
import { ErrorBoundary } from 'react-error-boundary';

// Reusable Fallback Component
function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div role="alert" style={{ color: 'orange', padding: '15px' }}>
      <p>The Weather Service is temporarily unavailable.</p>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Try Again</button>
    </div>
  );
}

const FloridaWeather = () => {
  // If this fails, the ErrorBoundary catches it
  const weather = undefined;
  return <div>Today's forecast: {weather.temp} degrees.</div>;
};

export default function WeatherApp() {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onReset={() => {
        // Reset the state of your app so the error doesn't happen again
        console.log("Retrying connection to NOAA servers...");
      }}
    >
      <FloridaWeather />
    </ErrorBoundary>
  );
}

This library is a game-changer because it provides a resetErrorBoundary function, allowing users to recover from errors without a full page refresh.

Best Practices for Error Handling in React

Over the years, I have developed a few “golden rules” for catching errors in functional components:

  1. Granularity: Don’t just wrap your whole app in one Error Boundary. Use multiple boundaries for different sections (e.g., Sidebar, Main Feed, Ads).
  2. User Experience: Always provide a way to “Try Again.” A dead-end error message is the fastest way to lose a user.
  3. Logging: In a production environment, use a service like LogRocket or Sentry within componentDidCatch to track errors occurring in the wild.
  4. Security: Never display raw system errors or stack traces to the user. Keep messages friendly and professional.

Error handling shouldn’t be an afterthought. It should be built into your component architecture from day one.

In this article, I showed you four different ways to handle errors in React functional components.

Using a combination of Error Boundaries for rendering errors and try-catch for logic/async errors will make your apps much more resilient.

I hope you found this guide helpful and can apply these patterns to your next project.

You may read:

51 Python Programs

51 PYTHON PROGRAMS PDF FREE

Download a FREE PDF (112 Pages) Containing 51 Useful Python Programs.

pyython developer roadmap

Aspiring to be a Python developer?

Download a FREE PDF on how to become a Python developer.

Let’s be friends

Be the first to know about sales and special discounts.