Skip to content

Commit fb1ed25

Browse files
author
Luca Forstner
authored
fix(nextjs): Don't capture suspense errors in server components (#12261)
1 parent d63d7c6 commit fb1ed25

File tree

3 files changed

+51
-2
lines changed

3 files changed

+51
-2
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as Sentry from '@sentry/nextjs';
2+
import { use } from 'react';
3+
export const dynamic = 'force-dynamic';
4+
5+
export default async function Page() {
6+
try {
7+
use(fetch('http://example.com/'));
8+
} catch (e) {
9+
Sentry.captureException(e); // This error should not be reported
10+
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for any async event processors to run
11+
await Sentry.flush();
12+
}
13+
14+
return <p>test</p>;
15+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForError, waitForTransaction } from '@sentry-internal/event-proxy-server';
3+
4+
test('should not capture serverside suspense errors', async ({ page }) => {
5+
const pageServerComponentTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
6+
return transactionEvent?.transaction === 'Page Server Component (/suspense-error)';
7+
});
8+
9+
let errorEvent;
10+
waitForError('nextjs-15', async transactionEvent => {
11+
return transactionEvent?.transaction === 'Page Server Component (/suspense-error)';
12+
}).then(event => {
13+
errorEvent = event;
14+
});
15+
16+
await page.goto(`/suspense-error`);
17+
18+
await page.waitForTimeout(5000);
19+
20+
const pageServerComponentTransaction = await pageServerComponentTransactionPromise;
21+
expect(pageServerComponentTransaction).toBeDefined();
22+
23+
expect(errorEvent).toBeUndefined();
24+
});

packages/nextjs/src/server/index.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,13 +190,23 @@ export function init(options: NodeOptions): void {
190190

191191
const originalException = hint.originalException;
192192

193-
const isReactControlFlowError =
193+
const isPostponeError =
194194
typeof originalException === 'object' &&
195195
originalException !== null &&
196196
'$$typeof' in originalException &&
197197
originalException.$$typeof === Symbol.for('react.postpone');
198198

199-
if (isReactControlFlowError) {
199+
if (isPostponeError) {
200+
// Postpone errors are used for partial-pre-rendering (PPR)
201+
return null;
202+
}
203+
204+
// We don't want to capture suspense errors as they are simply used by React/Next.js for control flow
205+
const exceptionMessage = event.exception?.values?.[0]?.value;
206+
if (
207+
exceptionMessage?.includes('Suspense Exception: This is not a real error!') ||
208+
exceptionMessage?.includes('Suspense Exception: This is not a real error, and should not leak')
209+
) {
200210
return null;
201211
}
202212

0 commit comments

Comments
 (0)