Stop showing raw chunk load errors (#23571)

On iOS Safari, a failed chunk load showed a snackbar containing the raw
browser string `Importing a module script failed.`

`PromiseRejectionEffect` snackbars the raw `error.message` of any
unhandled rejection, so a floating dynamic import leaked browser
internals to the user. It now skips the snackbar for stale chunk errors,
which are still captured by Sentry.

This only changes what the user sees, not the underlying fetch failure.
This commit is contained in:
Raphaël Bosi
2026-08-04 10:24:59 +02:00
committed by GitHub
parent 850d3d70fc
commit 6f6da14cc8
2 changed files with 77 additions and 1 deletions
@@ -1,5 +1,6 @@
import { useCallback, useEffect } from 'react';
import { checkIfItsAViteStaleChunkLazyLoadingError } from '@/error-handler/utils/checkIfItsAViteStaleChunkLazyLoadingError';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import {
CombinedGraphQLErrors,
@@ -44,7 +45,11 @@ export const PromiseRejectionEffect = () => {
error?.networkError?.name === 'AbortError' ||
error?.name === 'AbortError';
if (!isAbortError) {
const isViteStaleChunkLazyLoadingError =
error instanceof Error &&
checkIfItsAViteStaleChunkLazyLoadingError(error);
if (!isAbortError && !isViteStaleChunkLazyLoadingError) {
enqueueErrorSnackBar(
error instanceof Error ? { message: error.message } : {},
);
@@ -0,0 +1,71 @@
import { render, waitFor } from '@testing-library/react';
import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect';
jest.mock('@sentry/react', () => ({
captureException: jest.fn(),
}));
const enqueueErrorSnackBar = jest.fn();
jest.mock('@/ui/feedback/snack-bar-manager/hooks/useSnackBar', () => ({
useSnackBar: () => ({ enqueueErrorSnackBar }),
}));
const { captureException } = jest.requireMock('@sentry/react');
const dispatchUnhandledRejection = (reason: unknown) => {
const event = new Event('unhandledrejection');
Object.defineProperty(event, 'reason', { value: reason });
window.dispatchEvent(event);
};
describe('PromiseRejectionEffect', () => {
beforeEach(() => {
jest.clearAllMocks();
render(<PromiseRejectionEffect />);
});
it('should not snackbar a stale chunk error', async () => {
dispatchUnhandledRejection(new Error('Importing a module script failed.'));
await waitFor(() => {
expect(captureException).toHaveBeenCalledTimes(1);
});
expect(enqueueErrorSnackBar).not.toHaveBeenCalled();
});
it('should still snackbar an unrelated error', async () => {
dispatchUnhandledRejection(new Error('Some unrelated error'));
expect(enqueueErrorSnackBar).toHaveBeenCalledWith({
message: 'Some unrelated error',
});
await waitFor(() => {
expect(captureException).toHaveBeenCalledTimes(1);
});
});
it('should not snackbar an abort error', async () => {
dispatchUnhandledRejection({ name: 'AbortError' });
await waitFor(() => {
expect(captureException).toHaveBeenCalledTimes(1);
});
expect(enqueueErrorSnackBar).not.toHaveBeenCalled();
});
it('should snackbar a generic message when the reason is not an Error', async () => {
dispatchUnhandledRejection('something went wrong');
expect(enqueueErrorSnackBar).toHaveBeenCalledWith({});
await waitFor(() => {
expect(captureException).toHaveBeenCalledTimes(1);
});
});
});