diff --git a/.changeset/tricky-walls-walk.md b/.changeset/tricky-walls-walk.md
new file mode 100644
index 000000000000..9e58ec014a77
--- /dev/null
+++ b/.changeset/tricky-walls-walk.md
@@ -0,0 +1,5 @@
+---
+'@astrojs/react': patch
+---
+
+Prevent errors in React components from crashing the dev server
diff --git a/packages/astro/test/fixtures/react-component/src/components/ImportsThrowsAnError.jsx b/packages/astro/test/fixtures/react-component/src/components/ImportsThrowsAnError.jsx
new file mode 100644
index 000000000000..d6ff21dc3e40
--- /dev/null
+++ b/packages/astro/test/fixtures/react-component/src/components/ImportsThrowsAnError.jsx
@@ -0,0 +1,7 @@
+import ThrowsAnError from "./ThrowsAnError";
+
+export default function() {
+ return <>
+
+ >
+}
diff --git a/packages/astro/test/fixtures/react-component/src/components/ThrowsAnError.jsx b/packages/astro/test/fixtures/react-component/src/components/ThrowsAnError.jsx
new file mode 100644
index 000000000000..cf970e38c087
--- /dev/null
+++ b/packages/astro/test/fixtures/react-component/src/components/ThrowsAnError.jsx
@@ -0,0 +1,15 @@
+import { useState } from 'react';
+
+export default function() {
+ let player = undefined;
+ // This is tested in dev mode, so make it work during the build to prevent
+ // breaking other tests.
+ if(import.meta.env.MODE === 'production') {
+ player = {};
+ }
+ const [] = useState(player.currentTime || null);
+
+ return (
+
Should have thrown
+ )
+}
diff --git a/packages/astro/test/fixtures/react-component/src/pages/error-rendering.astro b/packages/astro/test/fixtures/react-component/src/pages/error-rendering.astro
new file mode 100644
index 000000000000..6984a6da5073
--- /dev/null
+++ b/packages/astro/test/fixtures/react-component/src/pages/error-rendering.astro
@@ -0,0 +1,11 @@
+---
+import ImportsThrowsAnError from '../components/ImportsThrowsAnError';
+---
+
+
+ Testing
+
+
+
+
+
diff --git a/packages/astro/test/react-component.test.js b/packages/astro/test/react-component.test.js
index e18f7129cc78..19bd42a431fb 100644
--- a/packages/astro/test/react-component.test.js
+++ b/packages/astro/test/react-component.test.js
@@ -94,6 +94,7 @@ describe('React Components', () => {
if (isWindows) return;
describe('dev', () => {
+ /** @type {import('./test-utils').Fixture} */
let devServer;
before(async () => {
@@ -145,5 +146,10 @@ describe('React Components', () => {
// test 1: react/jsx-runtime is used for the component
expect(jsxRuntime).to.be.ok;
});
+
+ it('When a nested component throws it does not crash the server', async () => {
+ const res = await fixture.fetch('/error-rendering');
+ await res.arrayBuffer();
+ });
});
});
diff --git a/packages/integrations/react/server.js b/packages/integrations/react/server.js
index c4e77bb910f0..6dd289c994cf 100644
--- a/packages/integrations/react/server.js
+++ b/packages/integrations/react/server.js
@@ -121,8 +121,11 @@ async function renderToPipeableStreamAsync(vnode) {
async function renderToStaticNodeStreamAsync(vnode) {
const Writable = await getNodeWritable();
let html = '';
- return new Promise((resolve) => {
+ return new Promise((resolve, reject) => {
let stream = ReactDOM.renderToStaticNodeStream(vnode);
+ stream.on('error', (err) => {
+ reject(err);
+ });
stream.pipe(
new Writable({
write(chunk, _encoding, callback) {