Repository: preactjs/preact (2025-08-08T15:17:31Z)

Issue:
the child component of ErrorBoundary, wrapped by memo function failed to recover from the last error
- [x] Check if updating to the latest Preact version resolves the issue

**Describe the bug**
the child componet wrapped by memo function caused an error in the first render, then it's parent component caught the error and solved the error, but this child component disappears in the next render.

**To Reproduce**

The second Test component disappeared.
[https://codesandbox.io/s/charming-curie-to3te?file=/src/index.js](https://codesandbox.io/s/charming-curie-to3te?file=/src/index.js)

Steps to reproduce the behavior:

Please see the demo above. It's not difficult to reproduce.

**Expected behavior**

all Test component should be rendered.



Discussion:
This is generally expected when you are using something outside of the closure. When you call `memo`, much like all the hooks stuff, you are leveraging the variables at that time. You can see that when you pass in `causeError` as a prop you won't be seeing this issue. https://codesandbox.io/s/headless-cherry-b5z3e?file=/src/index.js:82-582
Hi @JoviDeCroock , thanks for your quick reply. 
But, I'm still confused about your explanation. I don't think it's related to closures.

1. the same demo works as I expected in React. [https://stackblitz.com/edit/react-efu258?file=src/App.js](https://stackblitz.com/edit/react-efu258?file=src/App.js)
2. the value of `causeError` variable is always up-to-date in each render.
3. when you pass in causeError as a prop, it will only make the default `shouldComponentUpdate` of memorized component return `true`, then the diff for this component can continue. it is just a kind of workaround.

Am I right？



Patch:
diff --git a/src/diff/catch-error.js b/src/diff/catch-error.js
index 28325631fc..87c909c67a 100644
--- a/src/diff/catch-error.js
+++ b/src/diff/catch-error.js
@@ -3,7 +3,8 @@ import {
 	NULL,
 	COMPONENT_DIRTY,
 	COMPONENT_PENDING_ERROR,
-	COMPONENT_PROCESSING_EXCEPTION
+	COMPONENT_PROCESSING_EXCEPTION,
+	COMPONENT_FORCE
 } from '../constants';
 
 /**
@@ -28,6 +29,7 @@ export function _catchError(error, vnode, oldVNode, errorInfo) {
 			(component = vnode._component) &&
 			!(component._bits & COMPONENT_PROCESSING_EXCEPTION)
 		) {
+			component._bits |= COMPONENT_FORCE;
 			try {
 				ctor = component.constructor;
 


Test patch:
diff --git a/compat/test/browser/memo.test.js b/compat/test/browser/memo.test.js
index c32699436d..91b97170e1 100644
--- a/compat/test/browser/memo.test.js
+++ b/compat/test/browser/memo.test.js
@@ -11,7 +11,7 @@ import React, {
 	memo,
 	useState
 } from 'preact/compat';
-import { vi } from 'vitest';
+import { vi, expect } from 'vitest';
 
 const h = React.createElement;
 
@@ -238,4 +238,40 @@ describe('memo()', () => {
 
 		expect(Memoized.type).to.equal(Foo);
 	});
+
+	it('should recover and render siblings when memo child throws once', () => {
+		let causeError = true;
+
+		const TestWithMemo = /** @type {any} */ (memo(
+			/** @type {(props: { n: number }) => any} */ (props => {
+				const { n } = props;
+				if (n === 2 && causeError) {
+					throw new Error('test error');
+				}
+				return <p>test {n}</p>;
+			})
+		));
+
+		class App extends Component {
+			static getDerivedStateFromError() {
+				causeError = false;
+				return {};
+			}
+			render() {
+				return (
+					<div>
+						<h1>Example</h1>
+						<TestWithMemo n={1} />
+						<TestWithMemo n={2} />
+						<TestWithMemo n={3} />
+					</div>
+				);
+			}
+		}
+
+		render(<App />, scratch);
+		rerender();
+
+		expect(scratch.textContent).to.equal('Exampletest 1test 2test 3');
+	});
 });
diff --git a/test/browser/lifecycles/componentDidCatch.test.js b/test/browser/lifecycles/componentDidCatch.test.js
index 6308b140cb..4b0349210a 100644
--- a/test/browser/lifecycles/componentDidCatch.test.js
+++ b/test/browser/lifecycles/componentDidCatch.test.js
@@ -1,7 +1,7 @@
 import { setupRerender } from 'preact/test-utils';
 import { createElement, render, Component, Fragment } from 'preact';
 import { setupScratch, teardown } from '../../_util/helpers';
-import { vi } from 'vitest';
+import { vi, expect } from 'vitest';
 
 /** @jsx createElement */
 
@@ -714,5 +714,49 @@ describe('Lifecycle methods', () => {
 
 			expect(info).to.deep.equal({});
 		});
+
+		// New test mirroring React example but using shouldComponentUpdate instead of memo
+		it('should recover and render siblings when child throws once and uses shouldComponentUpdate', () => {
+			let causeError = true;
+
+			const Render = ({ n }) => <p>test {n}</p>;
+
+			class TestWithSCU extends Component {
+				shouldComponentUpdate(nextProps) {
+					return nextProps.n !== this.props.n;
+				}
+				render(props) {
+					const { n } = props;
+					if (n === 2 && causeError) {
+						throw new Error('test error');
+					}
+					return <Render n={n} />;
+				}
+			}
+
+			class App extends Component {
+				componentDidCatch() {
+					causeError = false;
+					this.setState({});
+				}
+				render() {
+					return (
+						<div>
+							<h1>Example</h1>
+							<TestWithSCU n={1} />
+							<TestWithSCU n={2} />
+							<TestWithSCU n={3} />
+						</div>
+					);
+				}
+			}
+
+			vi.spyOn(App.prototype, 'componentDidCatch');
+			render(<App />, scratch);
+			rerender();
+
+			expect(App.prototype.componentDidCatch).toHaveBeenCalledOnce();
+			expect(scratch.textContent).to.equal('Exampletest 1test 2test 3');
+		});
 	});
 });