Things go wrong in real apps: a server sends strange data, a component has a bug, a lazy file fails to load. Without a plan, one small error can turn the whole page white. In this lesson you will learn error boundaries, which keep the rest of the page working, and portals, which let a component draw somewhere else in the page, for example for a modal.
What happens when a component throws
If an error happens while React is rendering a component (for example user.name when user is null), React removes the whole component tree from the screen. The visitor sees a blank page. That is much worse than a small message in one box.
An error boundary is a component that catches such errors in the components below it, and shows a fallback instead. The rest of the page keeps working.
Writing an error boundary
Error boundaries are the one thing in React that still must be a class component, because there is no hook for it yet. You write it once and use it everywhere:
import { Component } from 'react';
export class ErrorBoundary extends Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
// Send this to a log or to an error service
console.error('Caught by boundary:', error, info.componentStack);
}
render() {
if (this.state.hasError) {
return this.props.fallback ?? <p role="alert">Something went wrong in this part of the page.</p>;
}
return this.props.children;
}
}
getDerivedStateFromErrorruns when a child throws, and setshasErrorto true, so the next render shows the fallback.componentDidCatchis where you report the error, to the console or to a monitoring service.rendershows the fallback when there is an error, and the normal children otherwise.
You can also use the ready-made package react-error-boundary, which gives you the same with a function-friendly API and a handy reset button. Many teams use it.
Using it
function App() {
return (
<div>
<Header />
<ErrorBoundary fallback={<p>The chart could not be shown.</p>}>
<SalesChart />
</ErrorBoundary>
<ErrorBoundary>
<Comments />
</ErrorBoundary>
</div>
);
}
If SalesChart crashes, only that box shows the message. The header and the comments stay. This is the idea: put boundaries around parts that can fail independently. A good plan is one boundary around the whole app as a last safety net, and smaller ones around risky parts such as widgets, charts and lazy pages.
What error boundaries do not catch
- Errors in event handlers (a click handler that throws). Use a normal
tryandcatchthere. - Errors in async code, such as inside
fetchpromises orsetTimeout. Catch them yourself and put the error in state, as in the fetching lesson. - Server rendering errors, and errors in the boundary itself.
function SaveButton() {
const [error, setError] = useState('');
async function handleClick() {
try {
await save();
} catch (err) {
setError('Could not save. Please try again.');
}
}
return (
<>
<button onClick={handleClick}>Save</button>
{error && <p role="alert">{error}</p>}
</>
);
}
A retry button
A fallback is more useful with a way to try again. Use a key to reset the boundary and its children:
function Widget() {
const [attempt, setAttempt] = useState(0);
return (
<ErrorBoundary
key={attempt}
fallback={<button onClick={() => setAttempt(attempt + 1)}>Something failed. Try again</button>}
>
<SalesChart />
</ErrorBoundary>
);
}
When attempt changes, the key changes, so React makes a brand new boundary with no error. The chart tries to load again.
Portals: draw somewhere else
A modal (a box that appears above the page) is a problem if it sits deep in your component tree. A parent with overflow: hidden or a z-index can cut it off or hide it. A portal lets a component render its output in a different place in the real page, such as directly under <body>, while still being part of the same React tree. That means props, state and context still work normally.
import { createPortal } from 'react-dom';
function Modal({ open, onClose, title, children }) {
if (!open) return null;
return createPortal(
<div className="backdrop" onClick={onClose}>
<div className="dialog" role="dialog" aria-modal="true" aria-label={title}
onClick={(e) => e.stopPropagation()}>
<h2>{title}</h2>
{children}
<button onClick={onClose}>Close</button>
</div>
</div>,
document.body
);
}
createPortal(jsx, container) draws the JSX inside container, here document.body. The click on the dark backdrop closes the modal, and stopPropagation makes sure that clicking inside the box does not.
Making a modal accessible
A modal that looks fine can still be hard to use. These points matter for people who use keyboards and screen readers:
- Give the box
role="dialog",aria-modal="true"and a name througharia-labeloraria-labelledby. - Move focus into the modal when it opens, and return focus to the button that opened it when it closes.
- Close with the Escape key.
- Trap the focus inside while it is open, so Tab does not move to the page behind.
- Stop the page behind from scrolling while the modal is open.
import { useEffect, useRef } from 'react';
function useEscape(onClose, active) {
useEffect(() => {
if (!active) return;
function handleKey(event) {
if (event.key === 'Escape') onClose();
}
document.addEventListener('keydown', handleKey);
return () => document.removeEventListener('keydown', handleKey);
}, [onClose, active]);
}
Getting all of these right by hand is work. The built-in HTML <dialog> element does a lot of it for you (focus handling, Escape and a backdrop) when opened with showModal(), and libraries like Radix UI or React Aria give tested, accessible modals. For production apps, using one of them is often the wisest choice.
Other good uses of portals
- Tooltips and dropdown menus that must not be clipped by a parent.
- Toast messages (small notices at a corner of the screen).
- Full-screen loading overlays.
Common mistakes
- Having no error boundary, so one bug makes the whole page white.
- Expecting a boundary to catch errors in click handlers or in
fetch. Usetryandcatch. - A fallback with no way to retry or leave.
- A modal without Escape, focus handling and a role.
- Using a modal when a simple inline message would be kinder.
Where to put error boundaries
A common question is how many boundaries an app needs. A good plan has three layers.
- One around the whole app. This is the last safety net. If everything else fails, the visitor sees a friendly page with a reload button, not a white screen.
- One around each page or route. If one page crashes, the menu and the layout stay, and the visitor can go to another page.
- Small ones around risky widgets, such as a chart, a map, a comments box or any part that depends on data you do not control.
Always report what the boundary catches. A boundary that hides errors but never tells you about them means you will not know that visitors are having problems. Send the error to a logging service, or at least to your server logs. Then fix the cause, not just the message.
Your task
- Add the
ErrorBoundaryto your app and create a component that throws on a button click state to test it. - Add a retry button using
key. - Build a
Modalwith a portal, an Escape key close and a backdrop click close. - Move focus into the modal on open, and back to the button on close.
How to check it worked
- A crash in one widget leaves the rest of the page working.
- Pressing Escape closes the modal.
- After the modal closes, focus returns to the button that opened it.
Summary
- An error boundary catches errors from components below it and shows a fallback instead of a white page.
- It does not catch errors in event handlers or async code, so use
tryandcatchthere. - A portal draws a component into another place in the page, such as
document.body, and is ideal for modals and tooltips. - Make modals accessible with a role, focus handling and Escape, or use a tested library.