Managing Errors: Professional Error Handling in React
Learn professional error handling in React. Discover how to catch API failures, store error messages in state, and provide clear UI feedback to your users.
Previously in this course, we covered handling loading states to keep users informed while waiting for data. Now, we’ll address the inevitable reality of web development: things go wrong. Whether it's a 404, a server outage, or a lost connection, your application needs to handle these failures gracefully rather than leaving the user staring at a blank screen.
Understanding Error Handling from First Principles
When you use the fetch API, it only rejects the promise if there is a network error (like the user being offline). If the server responds with a 404 Not Found or 500 Internal Server Error, fetch considers the request "successful."
To provide robust error handling, you must manually check the response.ok property. If it’s false, you throw an error yourself. By wrapping these operations in a try-catch block, you can intercept both network failures and your own custom errors, then store that information in your component's state to provide UI feedback.
Implementing Error Handling
Let’s update our MovieBrowser component. We need a new piece of state to hold the error message.
JSXimport { useState, useEffect } from CE9178">'react'; function MovieBrowser() { const [movies, setMovies] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); // New state for error useEffect(() => { async function fetchMovies() { setIsLoading(true); setError(null); // Reset error on new request try { const response = await fetch(CE9178">'https://api.example.com/movies'); // Manually check for HTTP errors if (!response.ok) { throw new Error(CE9178">'Failed to fetch movies. Please try again later.'); } const data = await response.json(); setMovies(data); } catch (err) { // Catch network errors or our thrown error setError(err.message); } finally { setIsLoading(false); } } fetchMovies(); }, []); if (error) return <div className="error-box">{error}</div>; if (isLoading) return <div>Loading...</div>; return ( <ul> {movies.map(movie => <li key={movie.id}>{movie.title}</li>)} </ul> ); }
Key Components of the Pattern
- Resetting State: Always call
setError(null)at the start of your effect. If a user retries a failed request, you don't want the old error message lingering on the screen. - The
finallyBlock: Use this to ensuresetIsLoading(false)runs regardless of whether the request succeeded or failed. This prevents your app from being stuck in a permanent loading state. - Conditional Rendering: By checking
if (error)before rendering the data, you prioritize informing the user that something went wrong.
While we are focusing on the frontend here, remember that designing error responses clients can actually use on the backend is equally important for a stable ecosystem. If you're interested in more advanced patterns, the TypeScript Result Pattern is a fantastic way to handle errors without relying on exceptions at all.
Hands-on Exercise
Modify your current movie browser project:
- Add an
errorstate variable. - Update your
fetchlogic to include atry-catchblock. - If
response.okis false, throw a descriptive error message. - In your JSX, add a conditional check: if
errorexists, render a paragraph with the error text in red.
Common Pitfalls
- Forgetting to throw: Beginners often forget that
fetchdoesn't throw on 404/500 errors. Always checkresponse.ok. - Ignoring the
finallyblock: If you only setsetIsLoading(false)inside thetryblock, your spinner will spin forever if the request crashes. - Over-sharing details: Don't show raw technical stack traces to your users. Keep the message helpful and human-readable, like "Something went wrong" rather than "Error 500: Internal Server Exception at line 42."
Recap
We've successfully moved from "happy path" coding to production-ready API interaction. By using try-catch and response.ok, we ensure that our app can recover from network issues. Storing these errors in state allows us to use conditional rendering to show the user exactly what they need to know, maintaining a professional and reliable user experience.
Up next: We will learn how to use cleanup functions in useEffect to prevent memory leaks and handle component unmounting.
Work with me

Headless WordPress + Next.js Frontend Development
Keep WordPress for content, get a lightning-fast Next.js frontend. The best of both worlds — familiar editing, modern speed.

React & Next.js Dashboard / Admin UI Development
A clean, data-rich dashboard UI in React or Next.js — charts, tables, and real-time data that your users will actually enjoy using.