From 3c48e27b2ea91c097f01a51b759a987c29caeeac Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?=
<71827178+bosiraphael@users.noreply.github.com>
Date: Mon, 27 Jul 2026 16:47:58 +0200
Subject: [PATCH] Fix stuck onboarding route on failed chunk preload (#23359)
Fixes [Sentry 7604159654](https://sentry.io/issues/7604159654/)
(v2.20.0, Mobile Safari). The onboarding router preloads 7 lazy chunks
on entry; Vite's CSS preload for SyncEmails rejected and three defects
compounded:
- `void SomePage.preload()` discarded the promise, so it became an
unhandled rejection and the user got a raw `Unable to preload CSS for
/assets/...css` snackbar.
- `lazyWithPreload` cached the *rejected* promise and rendered via
`throw preload()`. React pings on the rejection, re-renders, the
component throws the same settled rejected thenable, the ping listener
de-dupes, and the route hangs on its loader forever.
- `checkIfItsAViteStaleChunkLazyLoadingError` only matched Chrome's
message, so `AppErrorBoundary`'s reload recovery never fired for the
CSS-preload or Safari variants.
`lazyWithPreload` now records the failure in state instead of
rethrowing, so the thenable thrown into Suspense always fulfills,
`preload()` returns void and can never reject, and the render path
throws the real `Error` to the boundary, which reloads.
Two things worth knowing for review: `React.lazy` is not a substitute
here (its initializer has no synchronous fast path, so it suspends even
when the module is already loaded, reintroducing the loader flash #22392
removed), and the failure is deliberately sticky because Vite marks the
dep `seen` before attempting it, so an in-document retry loads the JS
without its CSS and silently renders an unstyled page.
---
.../app/hooks/useCreateWorkspaceAppRouter.tsx | 14 +-
...ItsAViteStaleChunkLazyLoadingError.test.ts | 28 +++
...eckIfItsAViteStaleChunkLazyLoadingError.ts | 11 +-
.../utils/__tests__/lazyWithPreload.test.tsx | 184 ++++++++++++++++++
.../src/utils/lazyWithPreload.tsx | 60 ++++--
5 files changed, 276 insertions(+), 21 deletions(-)
create mode 100644 packages/twenty-front/src/utils/__tests__/lazyWithPreload.test.tsx
diff --git a/packages/twenty-front/src/modules/app/hooks/useCreateWorkspaceAppRouter.tsx b/packages/twenty-front/src/modules/app/hooks/useCreateWorkspaceAppRouter.tsx
index 2aa9c1a8a7..8e206da6c1 100644
--- a/packages/twenty-front/src/modules/app/hooks/useCreateWorkspaceAppRouter.tsx
+++ b/packages/twenty-front/src/modules/app/hooks/useCreateWorkspaceAppRouter.tsx
@@ -124,13 +124,13 @@ const NotFound = lazy(() =>
);
const preloadOnboardingPages = () => {
- void WorkspaceActivation.preload();
- void CreateProfile.preload();
- void SyncEmails.preload();
- void InstallApps.preload();
- void InviteTeam.preload();
- void ChooseYourPlan.preload();
- void WorkspaceSetup.preload();
+ WorkspaceActivation.preload();
+ CreateProfile.preload();
+ SyncEmails.preload();
+ InstallApps.preload();
+ InviteTeam.preload();
+ ChooseYourPlan.preload();
+ WorkspaceSetup.preload();
return null;
};
diff --git a/packages/twenty-front/src/modules/error-handler/utils/__tests__/checkIfItsAViteStaleChunkLazyLoadingError.test.ts b/packages/twenty-front/src/modules/error-handler/utils/__tests__/checkIfItsAViteStaleChunkLazyLoadingError.test.ts
index 13879abee5..5dca4087be 100644
--- a/packages/twenty-front/src/modules/error-handler/utils/__tests__/checkIfItsAViteStaleChunkLazyLoadingError.test.ts
+++ b/packages/twenty-front/src/modules/error-handler/utils/__tests__/checkIfItsAViteStaleChunkLazyLoadingError.test.ts
@@ -11,6 +11,34 @@ describe('checkIfItsAViteStaleChunkLazyLoadingError', () => {
expect(result).toBe(true);
});
+ it('should return true for the Firefox dynamic import failure message', () => {
+ const error = new Error(
+ 'error loading dynamically imported module: /some/module.js',
+ );
+
+ const result = checkIfItsAViteStaleChunkLazyLoadingError(error);
+
+ expect(result).toBe(true);
+ });
+
+ it('should return true for the Safari dynamic import failure message', () => {
+ const error = new Error('Importing a module script failed.');
+
+ const result = checkIfItsAViteStaleChunkLazyLoadingError(error);
+
+ expect(result).toBe(true);
+ });
+
+ it('should return true when a CSS chunk fails to preload', () => {
+ const error = new Error(
+ 'Unable to preload CSS for /assets/SyncEmails-DKxn4rm-.css',
+ );
+
+ const result = checkIfItsAViteStaleChunkLazyLoadingError(error);
+
+ expect(result).toBe(true);
+ });
+
it('should return false when error message does not contain the Vite stale chunk error text', () => {
const error = new Error('Some other error message');
diff --git a/packages/twenty-front/src/modules/error-handler/utils/checkIfItsAViteStaleChunkLazyLoadingError.ts b/packages/twenty-front/src/modules/error-handler/utils/checkIfItsAViteStaleChunkLazyLoadingError.ts
index 39dd2f8aa8..f392e1360c 100644
--- a/packages/twenty-front/src/modules/error-handler/utils/checkIfItsAViteStaleChunkLazyLoadingError.ts
+++ b/packages/twenty-front/src/modules/error-handler/utils/checkIfItsAViteStaleChunkLazyLoadingError.ts
@@ -1,3 +1,12 @@
+const VITE_STALE_CHUNK_ERROR_MESSAGES = [
+ 'Failed to fetch dynamically imported module',
+ 'error loading dynamically imported module',
+ 'Importing a module script failed',
+ 'Unable to preload CSS for',
+];
+
export const checkIfItsAViteStaleChunkLazyLoadingError = (error: Error) => {
- return error.message.includes('Failed to fetch dynamically imported module');
+ return VITE_STALE_CHUNK_ERROR_MESSAGES.some((staleChunkErrorMessage) =>
+ error.message.includes(staleChunkErrorMessage),
+ );
};
diff --git a/packages/twenty-front/src/utils/__tests__/lazyWithPreload.test.tsx b/packages/twenty-front/src/utils/__tests__/lazyWithPreload.test.tsx
new file mode 100644
index 0000000000..d03c9e9e08
--- /dev/null
+++ b/packages/twenty-front/src/utils/__tests__/lazyWithPreload.test.tsx
@@ -0,0 +1,184 @@
+import { render, screen } from '@testing-library/react';
+import { Suspense, type ComponentType } from 'react';
+import { ErrorBoundary, type FallbackProps } from 'react-error-boundary';
+
+import { lazyWithPreload } from '~/utils/lazyWithPreload';
+
+const PRELOAD_ERROR_MESSAGE =
+ 'Unable to preload CSS for /assets/SyncEmails-DKxn4rm-.css';
+
+const PageContent = () =>