惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

L
Lohrmann on Cybersecurity
Martin Fowler
Martin Fowler
Engineering at Meta
Engineering at Meta
腾讯CDC
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Microsoft Azure Blog
Microsoft Azure Blog
G
Google Developers Blog
TaoSecurity Blog
TaoSecurity Blog
博客园_首页
Vercel News
Vercel News
Hugging Face - Blog
Hugging Face - Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Last Week in AI
Last Week in AI
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
T
The Exploit Database - CXSecurity.com
量子位
Project Zero
Project Zero
A
Arctic Wolf
小众软件
小众软件
NISL@THU
NISL@THU
C
CERT Recently Published Vulnerability Notes
有赞技术团队
有赞技术团队
MongoDB | Blog
MongoDB | Blog
博客园 - 聂微东
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
N
News and Events Feed by Topic
宝玉的分享
宝玉的分享
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
T
Troy Hunt's Blog
P
Privacy & Cybersecurity Law Blog
Security Latest
Security Latest
B
Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
D
DataBreaches.Net
Schneier on Security
Schneier on Security
The Hacker News
The Hacker News
K
Kaspersky official blog
C
Check Point Blog
Hacker News: Ask HN
Hacker News: Ask HN
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Webroot Blog
Webroot Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
人人都是产品经理
人人都是产品经理
AI
AI
Cisco Talos Blog
Cisco Talos Blog
MyScale Blog
MyScale Blog
Cloudbric
Cloudbric
B
Blog RSS Feed
S
Schneier on Security
P
Palo Alto Networks Blog

Kent C. Dodds Blog

Implementing Hybrid Semantic + Lexical Search Simplifying Containers with Cloudflare Sandboxes Migrating to Workspaces and Nx Offloading FFmpeg with Cloudflare Building Semantic Search on my Content Helping YOU ask ME questions with AI How I used Cursor to Migrate Frameworks The Dow's Start on the Covenant Path 2025 in Review The next chapter: EpicAI.pro AI is taking your job How I increased my visibility Launching Epic Web 2023 in Review Stop Being a Junior RSC with Dan Abramov and Joe Savona Live Stream Fixing a Memory Leak in a Production Node.js App 2022 in Review My Car Accident I Migrated from a Postgres Cluster to Distributed SQLite with LiteFS I'm building EpicWeb.dev A review of my time at Remix Remix: The Yang to React's Yin How I help you build better websites Why I Love Remix The State Initializer Pattern How to React ⚛️ Get a catch block error message with TypeScript Building an awesome image loading experience How Remix makes CSS clashes predictable Introducing the new kentcdodds.com How I built a modern website in 2021 How to use React Context effectively Static vs Unit vs Integration vs E2E Testing for Frontend Apps The Testing Trophy and Testing Classifications Array reduce vs chaining vs for loop Don't Solve Problems, Eliminate Them Super Simple Start to Remix Super Simple Start to ESModules in Node.js JavaScript Pass By Value Function Parameters How to write a Constrained Identity Function (CIF) in TypeScript How to optimize your context value How to write a React Component in TypeScript TypeScript Function Syntaxes Listify a JavaScript Array Build vs Buy: Component Libraries edition Using fetch with TypeScript Wrapping React.useState with TypeScript Define function overload types with TypeScript 2020 in Review Business and Engineering alignment Hi, thanks for reaching out to me 👋 useEffect vs useLayoutEffect Super simple start to Firebase functions Super simple start to Netlify functions Super Simple Start to css variables Favor Progress Over Pride in Open Source Testing Implementation Details How getting into Open Source has been awesome for me useState lazy initialization and function updates Use ternaries rather than && in JSX Application State Management with React JavaScript to Know for React How I structure Express apps What open source project should I contribute to? When I follow TDD AHA Programming 💡 How I Record Educational Videos Should I write a test or fix a bug? Stop mocking fetch Intentional Career Building Improve test error messages of your abstractions Tracing user interactions with React Eliminate an entire category of bugs with a few simple tools Common mistakes with React Testing Library Super Simple Start to React Stop using client-side route redirects The State Reducer Pattern with React Hooks Function forms Replace axios with a simple custom fetch wrapper How to test custom React hooks React Production Performance Monitoring Should I useState or useReducer? Stop using isLoading booleans Make Your Test Fail Make your own DevTools An Argument for Automation Fix the "not wrapped in act(...)" warning Super Simple Start to ESModules in the Browser Implementing a simple state machine library in JavaScript 2010s Decade in Review Why users care about how you write code Why I avoid nesting closures Don't call a React function component Why your team needs TestingJavaScript.com Inversion of Control Understanding React's key prop How to Enable React Concurrent Mode How to add testing to an existing project Profile a React App for Performance
Use react-error-boundary to handle errors in React
2020-07-20 · via Kent C. Dodds Blog

What's wrong with this code?

import * as React from 'react'
import ReactDOM from 'react-dom'

function Greeting({ subject }) {
	return <div>Hello {subject.toUpperCase()}</div>
}

function Farewell({ subject }) {
	return <div>Goodbye {subject.toUpperCase()}</div>
}

function App() {
	return (
		<div>
			<Greeting />
			<Farewell />
		</div>
	)
}

ReactDOM.render(<App />, document.getElementById('root'))

If you send that to production, your users are going to get the white screen of sadness:

Chrome window with nothing but white

If you run this with create-react-app's error overlay (during development), you'll get this:

TypeError Cannot read property 'toUpperCase' of undefined

The problem is we need to either pass a subject prop (as a string) or default the subject prop's value. Obviously, this is contrived, but runtime errors happen all of the time and that's why it's a good idea to gracefully handle such errors. So let's leave this error in for a moment and see what tools React has for us to handle runtime errors like this.

try/catch?

The naive approach to handling this kind of error would be to add a try/catch:

import * as React from 'react'
import ReactDOM from 'react-dom'

function ErrorFallback({ error }) {
	return (
		<div role="alert">
			<p>Something went wrong:</p>
			<pre style={{ color: 'red' }}>{error.message}</pre>
		</div>
	)
}

function Greeting({ subject }) {
	try {
		return <div>Hello {subject.toUpperCase()}</div>
	} catch (error) {
		return <ErrorFallback error={error} />
	}
}

function Farewell({ subject }) {
	try {
		return <div>Goodbye {subject.toUpperCase()}</div>
	} catch (error) {
		return <ErrorFallback error={error} />
	}
}

function App() {
	return (
		<div>
			<Greeting />
			<Farewell />
		</div>
	)
}

ReactDOM.render(<App />, document.getElementById('root'))

That "works":

But, it may be ridiculous of me, but what if I don't want to wrap every component in my app in a try/catch block? In regular JavaScript, you can simply wrap the calling function in a try/catch and it'll catch any errors in the functions it calls. Let's try that here:

import * as React from 'react'
import ReactDOM from 'react-dom'

function ErrorFallback({ error }) {
	return (
		<div role="alert">
			<p>Something went wrong:</p>
			<pre style={{ color: 'red' }}>{error.message}</pre>
		</div>
	)
}

function Greeting({ subject }) {
	return <div>Hello {subject.toUpperCase()}</div>
}

function Farewell({ subject }) {
	return <div>Goodbye {subject.toUpperCase()}</div>
}

function App() {
	try {
		return (
			<div>
				<Greeting />
				<Farewell />
			</div>
		)
	} catch (error) {
		return <ErrorFallback error={error} />
	}
}

ReactDOM.render(<App />, document.getElementById('root'))

Unfortunately, this doesn't work. And that's because we're not the ones calling Greeting and Farewell. React calls those functions. When we use them in JSX, we're simply creating React elements with those functions as the type. Telling React that "if the App is rendered, here are the other components that will need to be called." But we're not actually calling them, so the try/catch won't work.

I'm not too disappointed by this to be honest, because try/catch is inherently imperative and I'd prefer a declarative way to handle errors in my app anyway.

React Error Boundary

This is where the Error Boundary feature comes in to play. An "Error Boundary" is a special component that you write to handle runtime errors like those above. For a component to be an Error Boundary:

  1. It must be a class component 🙁
  2. It must implement either getDerivedStateFromError or componentDidCatch.

Luckily, we have react-error-boundary which exposes the last Error Boundary component anyone needs to write because it gives you all the tools you need to declaratively handle runtime errors in your React apps.

So let's add react-error-boundary and render the ErrorBoundary component:

import * as React from 'react'
import ReactDOM from 'react-dom'
import { ErrorBoundary } from 'react-error-boundary'

function ErrorFallback({ error }) {
	return (
		<div role="alert">
			<p>Something went wrong:</p>
			<pre style={{ color: 'red' }}>{error.message}</pre>
		</div>
	)
}

function Greeting({ subject }) {
	return <div>Hello {subject.toUpperCase()}</div>
}

function Farewell({ subject }) {
	return <div>Goodbye {subject.toUpperCase()}</div>
}

function App() {
	return (
		<div>
			<ErrorBoundary FallbackComponent={ErrorFallback}>
				<Greeting />
				<Farewell />
			</ErrorBoundary>
		</div>
	)
}

ReactDOM.render(<App />, document.getElementById('root'))

And that works perfectly:

Error Recovery

The nice thing about this is you can almost think about the ErrorBoundary component the same way you do a try/catch block. You can wrap it around a bunch of React components to handle lots of errors, or you can scope it down to a specific part of the tree to have more granular error handling and recovery. react-error-boundary gives us all the tools we need to manage this as well.

Here's a more complex example:

function ErrorFallback({ error, resetErrorBoundary }) {
	return (
		<div role="alert">
			<p>Something went wrong:</p>
			<pre style={{ color: 'red' }}>{error.message}</pre>
			<button onClick={resetErrorBoundary}>Try again</button>
		</div>
	)
}

function Bomb({ username }) {
	if (username === 'bomb') {
		throw new Error('💥 CABOOM 💥')
	}
	return `Hi ${username}`
}

function App() {
	const [username, setUsername] = React.useState('')
	const usernameRef = React.useRef(null)

	return (
		<div>
			<label>
				{`Username (don't type "bomb"): `}
				<input
					placeholder={`type "bomb"`}
					value={username}
					onChange={(e) => setUsername(e.target.value)}
					ref={usernameRef}
				/>
			</label>
			<div>
				<ErrorBoundary
					FallbackComponent={ErrorFallback}
					onReset={() => {
						setUsername('')
						usernameRef.current.focus()
					}}
					resetKeys={[username]}
				>
					<Bomb username={username} />
				</ErrorBoundary>
			</div>
		</div>
	)
}

Here's what that experience is like:

Username (don't type "bomb"):

Hi

You'll notice that if you type "bomb", the Bomb component is replaced by the ErrorFallback component and you can recover by either changing the username (because that's in the resetKeys prop) or by clicking "Try again" because that's wired up to resetErrorBoundary and we have an onReset that resets our state to a username that won't trigger the error all over again.

Handle all errors

Unfortunately, there are some errors that React doesn't/can't hand off to our Error Boundary. To quote the React docs:

Error boundaries do not catch errors for:

  • Event handlers (learn more)
  • Asynchronous code (e.g. setTimeout or requestAnimationFrame callbacks)
  • Server side rendering
  • Errors thrown in the error boundary itself (rather than its children)

Most of the time, folks will manage some error state and render something different in the event of an error, like so:

function Greeting() {
	const [{ status, greeting, error }, setState] = React.useState({
		status: 'idle',
		greeting: null,
		error: null,
	})

	function handleSubmit(event) {
		event.preventDefault()
		const name = event.target.elements.name.value
		setState({ status: 'pending' })
		fetchGreeting(name).then(
			(newGreeting) => setState({ greeting: newGreeting, status: 'resolved' }),
			(newError) => setState({ error: newError, status: 'rejected' }),
		)
	}

	return status === 'rejected' ? (
		<ErrorFallback error={error} />
	) : status === 'resolved' ? (
		<div>{greeting}</div>
	) : (
		<form onSubmit={handleSubmit}>
			<label>Name</label>
			<input id="name" />
			<button type="submit" onClick={handleClick}>
				get a greeting
			</button>
		</form>
	)
}

Unfortunately, doing things that way means that you have to maintain TWO ways to handle errors:

  1. Runtime errors
  2. fetchGreeting errors

Luckily, react-error-boundary also exposes a simple hook to help with these situations as well. Here's how you could use that to side-step this entirely:

function Greeting() {
	const [{ status, greeting }, setState] = React.useState({
		status: 'idle',
		greeting: null,
	})
	const { showBoundary } = useErrorBoundary()

	function handleSubmit(event) {
		event.preventDefault()
		const name = event.target.elements.name.value
		setState({ status: 'pending' })
		fetchGreeting(name).then(
			(newGreeting) => setState({ greeting: newGreeting, status: 'resolved' }),
			(error) => showBoundary(error),
		)
	}

	return status === 'resolved' ? (
		<div>{greeting}</div>
	) : (
		<form onSubmit={handleSubmit}>
			<label>Name</label>
			<input id="name" />
			<button type="submit" onClick={handleClick}>
				get a greeting
			</button>
		</form>
	)
}

So when our fetchGreeting promise is rejected, the handleError function is called with the error and react-error-boundary will make that propagate to the nearest error boundary like usual.

Alternatively, let's say you're using a hook that gives you the error:

function Greeting() {
	const [name, setName] = React.useState('')
	const { status, greeting, error } = useGreeting(name)
	if (error) throw error

	function handleSubmit(event) {
		event.preventDefault()
		const name = event.target.elements.name.value
		setName(name)
	}

	return status === 'resolved' ? (
		<div>{greeting}</div>
	) : (
		<form onSubmit={handleSubmit}>
			<label>Name</label>
			<input id="name" />
			<button type="submit" onClick={handleClick}>
				get a greeting
			</button>
		</form>
	)
}

In this case, if the error is ever set to a truthy value, then it will be propagated to the nearest error boundary.

In either case, you could handle those errors like this:

const ui = (
	<ErrorBoundary FallbackComponent={ErrorFallback}>
		<Greeting />
	</ErrorBoundary>
)

And now that'll handle your runtime errors as well as the async errors in the fetchGreeting or useGreeting code.

Conclusion

Error Boundaries have been a feature in React for years and we're still in this awkward situation of handling runtime errors with Error Boundaries and handling other error states within our components when we would be much better off reusing our Error Boundary components for both. If you haven't already given react-error-boundary a try, definitely give it a solid look!

Good luck.

Oh, one other thing. Right now, you may notice that you'll experience that error overlay even if the error was handled by your Error Boundary. This will only happen during development (if you're using a dev server that supports it, like react-scripts, gatsby, or codesandbox). It won't show up in production. Yes, I agree this is annoying. PRs welcome.