A thrown error makes React call your components again

I am still getting caught out by React and I fully expect that to continue!

It turns out that when a component throws an error during rendering or in an effect, React calls every component in the stack again. It also calls them with NO props, which affects what code runs.

Proving it

window.parentCalls = [];

function Parent(props) {
	window.parentCalls.push(
		props === undefined ? 'Parent called with no props' : 'Parent called with props'
	);
	return props.children;
}

function Child() {
	throw new Error('boom');
}

export default function App() {
	return (
		<Parent>
			<Child />
		</Parent>
	);
}

// Log once React has finished handling the error
setTimeout(() => console.log(window.parentCalls.join('\n')), 1000);

Running this on CodePen (React 19, production build) logs:

Parent called with props
Parent called with no props
Parent called with no props
Parent called with props
Parent called with no props
Parent called with no props

Parent renders normally with its props, then React calls it again with no props. React then retries the render once, which is why the pattern repeats. The number of no-props calls depends on the React version and on whether it’s a development or production build, but there is always at least one.

The example records each call and logs them afterwards, because React silences console.log during these calls in development.

Which code runs again

To reiterate, when a component throws an error during rendering or in an effect, React calls the component that threw and each parent component up the stack with NO props. This is an important detail as any code at the top of a component that does not read props and exists before the first hook will run again. For example:

function Parent(props) {
	// Code here runs again
	setUp();

	// props is undefined, so this throws and nothing below it runs
	const title = props.title;
}

Hooks work the same way. React switches them off during these calls, so the first hook throws and stops the component.

function Parent() {
	// Code here runs again
	setUp();

	// Hooks are switched off, so this throws and nothing below it runs
	const [count, setCount] = useState(0);

	// This does not run again
	setUpSomethingElse();
}

If a component destructures its props, nothing in it runs at all. Destructuring undefined throws before the first line of the component, so setUp() is never called.

// props is undefined, so this throws before the component body runs
function Parent({ title }) {
	// This does not run again
	setUp();
}

This can hide the problem. Change { title } to props, and setUp() starts running again whenever an error is thrown.

The footgun

The point being, any setup code that runs again is a candidate for a bug after an error has been thrown. The real problem is the fact that setUp() is where it is, regardless of any thrown errors, because components re-render all the time. However, if something like that slips through in the root component, which ordinarily is only ever called once, it is harmless until an error is thrown and it unwittingly gets invoked again. This is the footgun!

What that looks like depends on what setUp() does. If it adds event listeners, every click is now handled two or more times, so a button might add the same item to a basket twice. If it opens a connection, sends analytics or creates a store, you get duplicate connections, double-counted events or state that has been reset. None of it points back to the root component, so the bugs look unrelated to the error that caused them. In my case, those bugs did show up, and they were fixed in the wrong place because I didn’t know this was the root cause.

I have been doing this long enough that I should have spotted setUp() was in the wrong place. But I missed it, and I assumed the root component would only ever be called once. Naive! Once I understood that every component in the stack gets called again when an error is thrown, it became clear what was happening. What also became clear is that it could easily have stayed hidden. If the code had been in a different order, say with a hook before the setUp() call, it would never have run again.

Avoiding it

React recommends keeping render code pure, so it is safe to run any number of times, and putting setup code outside the component.

Code outside the component runs once, when the file loads. It isn’t part of the component, so when an error is thrown, React’s extra calls to App never reach it. Normal re-renders cannot run it again either.

// Runs once when the file loads. React never calls this.
setUp();

function App() {
	// ...
}

Keep setup code out of the render body, and a thrown error has nothing to run again.