# Error Handling

Course: [React](https://www.devtutorials.ai/learn/react) · Section: Application Quality
Source: https://www.devtutorials.ai/learn/react/quality/error-handling

> React errors, try/catch with async code, API error handling, friendly error messages, error boundaries, fallback UI, debugging and React Developer Tools.

## Understanding React Errors

An **error** is something that stops your code from working as planned. Every app has errors. A good app catches them and tells the user what happened.

In a React app, errors come from two main places:

- **Render errors.** A component fails while it builds its TSX, for example by reading `.name` of `undefined`. React cannot draw that part of the page.
- **Async errors.** A request to a server fails, or the server sends back an error code. This happens later, inside an event handler or an effect.

React handles these two kinds in different ways. This lesson shows both.

### Real-World Example

In a bookshop app, one book from the server has no author, so `BookCard` throws an error while it renders. Without protection, React removes the whole page and the user sees a blank screen.

The team wraps each section in an `ErrorBoundary`. Now only that section shows "Something went wrong", and the rest of the shop keeps working.

Run the lab below to see the problem. `BookCard` fails, and React removes the **whole** page, not only that card. The console shows the error.

      <h1>Book Corner</h1>
      {books.map((book) => <BookCard key={book.title} book={book} />)}
    </div>
  );
}
`,
    "BookCard.tsx": `interface Book {
  title: string;
  author?: { name: string };
}

  // book.author is missing for one book, so .name fails
  return (
    <div className="card">
      <strong>{book.title}</strong> by {book.author!.name}
    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

When a component throws while rendering, React removes the whole tree. It does this on purpose: a broken screen with wrong data is worse than no screen. You fix this with an error boundary, later in this lesson.

## Try-Catch with Async Operations

`try` and `catch` are plain JavaScript. You put the risky code in `try`. If any line in it throws an error, JavaScript jumps to `catch`. `finally` runs at the end in both cases.

```tsx
try {
  const response = await fetch(url);   // may fail
  setBooks(await response.json());
} catch (error) {
  setError("Could not load books.");   // runs only on failure
} finally {
  setIsLoading(false);                 // always runs
}
```

<figure className="react-fig">
<svg viewBox="0 0 520 150" role="img" aria-labelledby="fig-e-trycatch" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-e-trycatch">try runs the risky code. If it fails, catch runs. finally always runs at the end</title>
  <rect x="10" y="55" width="110" height="46" rx="8" style={{ fill: "var(--accent-soft)", stroke: "var(--accent)", strokeWidth: 1.5 }} />
  <text x="65.0" y="75.0" textAnchor="middle" style={{ fill: "var(--accent)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>try</text>
  <text x="65.0" y="92.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>fetch data</text>
  <path d="M120 68 L190 30" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M190 30 L184.4 37.0 L181.0 30.9 Z" style={{ fill: "var(--faint)" }} />
  <text x="155.0" y="36" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 11, fontFamily: "var(--mono)" }}>ok</text>
  <rect x="195" y="8" width="150" height="44" rx="8" style={{ fill: "var(--ok-soft)", stroke: "var(--ok)", strokeWidth: 1.5 }} />
  <text x="270.0" y="35.0" textAnchor="middle" style={{ fill: "var(--ok)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>show the data</text>
  <path d="M120 90 L190 124" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M190 124 L181.0 123.5 L184.1 117.2 Z" style={{ fill: "var(--faint)" }} />
  <text x="155.0" y="134" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 11, fontFamily: "var(--mono)" }}>fails</text>
  <rect x="195" y="102" width="150" height="44" rx="8" style={{ fill: "var(--bad-soft)", stroke: "var(--bad)", strokeWidth: 1.5 }} />
  <text x="270.0" y="121.0" textAnchor="middle" style={{ fill: "var(--bad)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>catch</text>
  <text x="270.0" y="138.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>show a message</text>
  <path d="M345 30 L395 68" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M395 68 L386.3 65.8 L390.5 60.2 Z" style={{ fill: "var(--faint)" }} />
  <path d="M345 124 L395 90" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M395 90 L390.1 97.6 L386.2 91.8 Z" style={{ fill: "var(--faint)" }} />
  <rect x="400" y="55" width="110" height="46" rx="8" style={{ fill: "var(--accent-secondary-soft)", stroke: "var(--accent-secondary)", strokeWidth: 1.5 }} />
  <text x="455.0" y="75.0" textAnchor="middle" style={{ fill: "var(--accent-secondary)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>finally</text>
  <text x="455.0" y="92.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>stop loading</text>
  <circle r="6" style={{ fill: "var(--ok)" }}>
    <animateMotion dur="3s" repeatCount="indefinite" path="M120 68 L190 30 M345 30 L395 68" />
  </circle>
  <circle r="6" style={{ fill: "var(--bad)" }}>
    <animateMotion dur="3s" repeatCount="indefinite" path="M120 90 L190 124 M345 124 L395 90" />
  </circle>
</svg>
<figcaption>Use try and catch around every await that can fail.</figcaption>
</figure>

Below, the button calls a server address that does not exist. `fetch` throws, so `catch` runs and shows a message. Without `try`, the error would go to the console and the page would stay on "Loading..." for ever.

      <button onClick={() => load("https://api.bookcorner.dev/books")}>Good address</button>{" "}
      <button onClick={() => load("https://no-such-server.dev/books")}>Bad address</button>
      <p>{status}</p>
    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

In TypeScript, the value in `catch (error)` has the type `unknown`, because anything can be thrown. Write `(error as Error).message` to read its message.

## API Error Handling

`fetch` only throws when the request cannot be sent at all, for example when there is no network. When the server answers with an error code, such as **404** (not found) or **500** (server failed), `fetch` does **not** throw. You must check `response.ok` yourself.

A good pattern is to throw your own error when `response.ok` is false. Then one `catch` handles every kind of failure:

```tsx
if (!response.ok) {
  throw new Error("Status " + response.status);
}
```

      <button onClick={() => load("/books/1")}>Book 1</button>{" "}
      <button onClick={() => load("/books/99")}>Book 99</button>{" "}
      <button onClick={() => load("/error")}>Broken server</button>
      {title && <p>{title}</p>}
      {error && <p style={{ color: "crimson" }}>{error}</p>}
    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

## User-Friendly Error Messages

"Status 500" means nothing to most users. A good error message says, in plain words:

- **What happened:** "We could not load the books."
- **What to do next:** "Please check your internet and try again."

Keep the technical details for the console, where developers can see them. Show the user a short, kind message.

<figure className="react-fig">
<svg viewBox="0 0 520 110" role="img" aria-labelledby="fig-e-friendly" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-e-friendly">Turn technical error text into a message a user understands</title>
  <rect x="10" y="30" width="220" height="50" rx="8" style={{ fill: "var(--bad-soft)", stroke: "var(--bad)", strokeWidth: 1.5 }} />
  <text x="120.0" y="52.0" textAnchor="middle" style={{ fill: "var(--bad)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>HTTP 500</text>
  <text x="120.0" y="69.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>Internal Server Error</text>
  <path d="M230 55 L285 55" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M285 55 L276.7 58.5 L276.7 51.5 Z" style={{ fill: "var(--faint)" }} />
  <rect x="290" y="30" width="220" height="50" rx="8" style={{ fill: "var(--ok-soft)", stroke: "var(--ok)", strokeWidth: 1.5 }} />
  <text x="400.0" y="52.0" textAnchor="middle" style={{ fill: "var(--ok)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Something went wrong</text>
  <text x="400.0" y="69.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>Please try again.</text>
  <circle r="6" style={{ fill: "var(--accent-secondary)" }}>
    <animateMotion dur="2s" repeatCount="indefinite" path="M230 55 L285 55" />
  </circle>
</svg>
<figcaption>Users need to know what happened and what to do next, not the error code.</figcaption>
</figure>

A small function can turn a status code into a friendly message. Then every screen uses the same words.

      <button onClick={() => load("/books/2")}>Book 2</button>{" "}
      <button onClick={() => load("/books/99")}>Book 99</button>{" "}
      <button onClick={() => load("/error")}>Broken server</button>
      <p>{message}</p>
    </div>
  );
}
`,
    "errors.ts": `export function friendlyMessage(status: number): string {
  if (status === 404) return "We could not find that book. It may have been removed.";
  if (status >= 500) return "Our server has a problem. Please try again in a minute.";
  return "Something went wrong. Please try again.";
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

## Error Boundaries (Concept)

`try` and `catch` cannot catch render errors, because React calls your component, not your code. For render errors, React has **error boundaries**.

An **error boundary** is a component that wraps other components. If any component inside it throws while rendering, the boundary catches the error and shows a **fallback** (a backup screen) instead. The rest of the page keeps working.

<figure className="react-fig">
<svg viewBox="0 0 520 200" role="img" aria-labelledby="fig-e-crash" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-e-crash">Without an error boundary, one broken component blanks the whole page. With one, only that part shows a fallback</title>
  <text x="130" y="20" textAnchor="middle" style={{ fill: "var(--bad)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Without a boundary</text>
  <text x="390" y="20" textAnchor="middle" style={{ fill: "var(--ok)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>With a boundary</text>
  <rect x="20" y="30" width="220" height="160" rx="10" style={{ fill: "var(--panel2)", stroke: "var(--faint)" }} />
  <g>
  <rect x="35" y="45" width="190" height="30" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="130.0" y="65.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Header</text>
  <rect x="35" y="85" width="190" height="30" rx="8" style={{ fill: "var(--bad-soft)", stroke: "var(--bad)", strokeWidth: 1.5 }} />
  <text x="130.0" y="105.0" textAnchor="middle" style={{ fill: "var(--bad)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>BookCard (breaks)</text>
  <rect x="35" y="125" width="190" height="30" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="130.0" y="145.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Footer</text>
    <animate attributeName="opacity" values="1;1;0;0;1" keyTimes="0;0.35;0.45;0.9;1" dur="5s" repeatCount="indefinite" />
  </g>
  <text opacity="0" x="130" y="115" textAnchor="middle" style={{ fill: "var(--bad)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>blank page<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.35;0.45;0.9;1" dur="5s" repeatCount="indefinite" /></text>
  <rect x="280" y="30" width="220" height="160" rx="10" style={{ fill: "var(--panel2)", stroke: "var(--faint)" }} />
  <rect x="295" y="45" width="190" height="30" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="390.0" y="65.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Header</text>
  <rect x="295" y="85" width="190" height="30" rx="8" style={{ fill: "var(--ok-soft)", stroke: "var(--ok)", strokeWidth: 1.5 }} />
  <text x="390.0" y="105.0" textAnchor="middle" style={{ fill: "var(--ok)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Sorry, this part failed</text>
  <rect x="295" y="125" width="190" height="30" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="390.0" y="145.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Footer</text>
  <rect x="295" y="85" width="190" height="30" rx="8" style={{ fill: "none", stroke: "var(--ok)", strokeWidth: 3 }} opacity="0">
    <animate attributeName="opacity" values="0;1;0;0" keyTimes="0;0.15;0.4;1" dur="5s" begin="1.8s" repeatCount="indefinite" />
  </rect>
</svg>
<figcaption>An error boundary keeps the problem inside a small box.</figcaption>
</figure>

An error boundary must be a **class component**. This is the one place where React still needs a class. You write it once and reuse it everywhere. Look at the `ErrorBoundary.tsx` tab:

- `static getDerivedStateFromError()` runs when a child fails. It returns `{ hasError: true }`, so the next render shows the fallback.
- `componentDidCatch(error, info)` also runs. Use it to log the error.
- `render()` shows `this.props.fallback` when `hasError` is true, or the children when all is well.

Now each card is wrapped in its own boundary. Only the broken card shows the fallback.

      <h1>Book Corner</h1>
      {books.map((book) => (
      ))}
    </div>
  );
}
`,
    "ErrorBoundary.tsx": `import { Component, type ErrorInfo, type ReactNode } from "react";

interface Props {
  children: ReactNode;
  fallback: ReactNode;
}

interface State {
  hasError: boolean;
}

  state: State = { hasError: false };

  // 1. An error happened below: switch to the fallback
  static getDerivedStateFromError(): State {
    return { hasError: true };
  }

  // 2. A good place to log the error
  componentDidCatch(error: Error, info: ErrorInfo) {
    console.log("Caught by boundary: " + error.message);
  }

  render() {
    if (this.state.hasError) return this.props.fallback;
    return this.props.children;
  }
}
`,
    "BookCard.tsx": `interface Book {
  title: string;
  author?: { name: string };
}

  // book.author is missing for one book, so .name fails
  return (
    <div className="card">
      <strong>{book.title}</strong> by {book.author!.name}
    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Error boundaries catch errors while rendering. They do **not** catch errors in event handlers (like `onClick`) or in async code (like `fetch`). Use `try` and `catch` for those.

## Fallback UI Patterns

The **fallback UI** is what the user sees when something fails. Some common patterns:

- **Small inline message.** Replace only the broken part, as with the book card above.
- **Section fallback.** Wrap a whole area, such as a sidebar, so the rest of the page still works.
- **Retry button.** Let the user try again. This works well for failed requests.
- **Empty state.** When the data is just empty, say so ("No books yet") instead of showing an error.

The lab below shows the retry pattern. The first request fails on purpose. Click **Try again** to load the books.

        <p>{error}</p>
        <button onClick={() => setTries(tries + 1)}>Try again</button>
      </div>
    );
  }

  return (
    <ul>
      {books.map((b) => <li key={b.id}>{b.title}</li>)}
    </ul>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

## Debugging Techniques

**Debugging** means finding out why your code does not work, and fixing it. Some simple habits help a lot:

- **Read the error message first.** It names the problem and often the file and line. "Cannot read properties of undefined (reading 'name')" means something before `.name` was `undefined`.
- **Log values.** Put `console.log("book:", book)` just before the line that fails. Check that the value is what you expect.
- **Check the Network tab.** In the browser developer tools (press F12), the Network tab shows every request, its status code and its response.
- **Use `debugger`.** Write `debugger;` in your code. With the developer tools open, the browser pauses on that line, and you can look at every variable.
- **Change one thing at a time.** Then you know which change fixed the problem.

Easy Way to Remember: read the message, log the value, check the network, change one thing.

## React Developer Tools

**React Developer Tools** is a free browser extension for Chrome, Firefox and Edge. It adds two tabs to the browser's developer tools:

- **Components** shows your component tree. Click a component to see its props, state and hooks. You can even change a value and watch the page update.
- **Profiler** records which components render, and how long each render takes. You will use it in the next lesson.

<figure className="react-fig">
<svg viewBox="0 0 520 190" role="img" aria-labelledby="fig-e-devtools" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-e-devtools">React Developer Tools shows the component tree, and the props and state of the selected component</title>
  <rect x="10" y="10" width="500" height="170" rx="10" style={{ fill: "var(--panel2)", stroke: "var(--faint)" }} />
  <rect x="10" y="10" width="500" height="28" rx="10" style={{ fill: "var(--accent-soft)" }} />
  <text x="30" y="29" textAnchor="start" style={{ fill: "var(--accent)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>⚛ Components</text>
  <text x="150" y="29" textAnchor="start" style={{ fill: "var(--muted)", fontSize: 12, fontWeight: 400, fontFamily: "var(--sans)" }}>⚛ Profiler</text>
  <path d="M250 38 L250 180" style={{ stroke: "var(--faint)" }} />
  <text x="25" y="62" textAnchor="start" style={{ fill: "var(--ink2)", fontSize: 12, fontWeight: 400, fontFamily: "var(--mono)" }}>▾ App</text>
  <text x="45" y="84" textAnchor="start" style={{ fill: "var(--ink2)", fontSize: 12, fontWeight: 400, fontFamily: "var(--mono)" }}>Header</text>
  <text x="45" y="106" textAnchor="start" style={{ fill: "var(--ink2)", fontSize: 12, fontWeight: 400, fontFamily: "var(--mono)" }}>▾ BookList</text>
  <rect x="60" y="114" width="170" height="20" rx="4" style={{ fill: "var(--accent-soft)" }} />
  <text x="68" y="128" textAnchor="start" style={{ fill: "var(--accent)", fontSize: 12, fontWeight: 700, fontFamily: "var(--mono)" }}>BookCard</text>
  <text x="68" y="150" textAnchor="start" style={{ fill: "var(--ink2)", fontSize: 12, fontWeight: 400, fontFamily: "var(--mono)" }}>BookCard</text>
  <text x="265" y="62" textAnchor="start" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>BookCard</text>
  <text x="265" y="86" textAnchor="start" style={{ fill: "var(--muted)", fontSize: 11, fontWeight: 700, fontFamily: "var(--sans)" }}>props</text>
  <text x="275" y="106" textAnchor="start" style={{ fill: "var(--ink2)", fontSize: 12, fontWeight: 400, fontFamily: "var(--mono)" }}>title: "Wings of Fire"</text>
  <text x="275" y="126" textAnchor="start" style={{ fill: "var(--ink2)", fontSize: 12, fontWeight: 400, fontFamily: "var(--mono)" }}>price: 350</text>
  <text x="265" y="150" textAnchor="start" style={{ fill: "var(--muted)", fontSize: 11, fontWeight: 700, fontFamily: "var(--sans)" }}>hooks</text>
  <text x="275" y="170" textAnchor="start" style={{ fill: "var(--ink2)", fontSize: 12, fontWeight: 400, fontFamily: "var(--mono)" }}>State: false</text>
  <rect x="60" y="114" width="170" height="20" rx="8" style={{ fill: "none", stroke: "var(--accent-secondary)", strokeWidth: 3 }} opacity="0">
    <animate attributeName="opacity" values="0;1;0;0" keyTimes="0;0.15;0.4;1" dur="3s" begin="0s" repeatCount="indefinite" />
  </rect>
</svg>
<figcaption>Click a component on the left to see its props and state on the right.</figcaption>
</figure>

The playground on this page runs inside a sandbox, so the extension cannot see it. Install the extension, run a React app on your own computer with `npm run dev`, then press F12 and open the **Components** tab.

## Common mistakes

<div className="tscroll">

| Mistake | What you see | Fix |
|---|---|---|
| No `try`/`catch` around `await fetch(...)` | Loading never ends, and an error shows in the console | Wrap the request in `try`/`catch`, and use `finally` |
| Expecting `fetch` to throw on 404 or 500 | Error pages are used as data | Check `response.ok` and throw your own error |
| Showing `error.message` or a status code to users | Users see "Status 500" | Show a short, friendly message. Log the details |
| Writing an error boundary as a function component | It does not catch anything | Use a class with `getDerivedStateFromError` |
| Expecting a boundary to catch `onClick` or `fetch` errors | The error is not caught | Use `try`/`catch` inside the handler |
| One boundary around the whole app only | Any small error hides the whole page | Also wrap smaller parts, such as each widget |

</div>

## Check Yourself

## Exercises

## What to Remember

- Render errors break the component tree. Async errors happen later, in handlers and effects.
- Use `try`/`catch`/`finally` around `await`. Check `response.ok`, and show short, friendly messages.
- An error boundary (a class component) catches render errors in its children and shows a fallback. Debug with the console, the Network tab and React Developer Tools.

Next: Performance Optimization
