Every route in my SPA is code split with React.lazy. If a route's chunk fails to download because
the device went offline, that route stays permanently broken for the lifetime of the document, and
the "Retry" button in my error boundary cannot recover it.
Environment
- React
19.2.5 react-router-dom(client-side routing)- Vite (code splitting via dynamic
import()) react-error-boundary^4- Runs inside a hybrid WebView shell (Android WebView + iOS WKWebView)
Observed in production on device, and reproduced locally against the dev server.
Setup
lazy() is called once per route at module scope:
// src/app/router/index.jsx
import { lazy } from 'react';
const MedicalManagementPage = lazy(() => import('@/pages/medical/medical-management-main'));
const WeightTrendPage = lazy(() => import('@/pages/weight/weight-trend'));
// ...~60 more routes
Routes render inside an error boundary and Suspense:
<ErrorBoundary onReset={reset} fallbackRender={(props) => <ErrorFallback {...props} />}>
<Suspense fallback={<Loader />}>
<Outlet />
</Suspense>
</ErrorBoundary>
The fallback exposes the standard retry:
<button type='button' onClick={resetErrorBoundary}>
Retry
</button>
Steps to reproduce
- Sit on a route whose chunk is already loaded (Home). Everything works.
- Drop the network.
- Navigate to a route whose chunk has not been downloaded yet. The dynamic
import()fails and the error boundary renders the fallback. - Restore the network.
- Click Retry.
Expected: the import is attempted again and the route renders.
Actual: nothing happens. No HTTP request appears in the network tab, no Suspense fallback is
shown, no state changes. The error fallback stays on screen.
Additional observations:
- Other routes keep working normally after the network recovers. Only the route that failed is stuck.
- Navigating away and back to that route immediately re-renders the fallback, again with no request.
- Waiting 5+ minutes before returning changes nothing (so this is not a data-fetching cache issue — no query is involved at this point).
- On Android the hardware back button lets the user leave the screen. On iOS there is no back affordance, so the user is stuck until the app is killed.
Stack trace (Chromium):
TypeError: Failed to fetch dynamically imported module:
http://localhost:9000/src/pages/medical/medical-management-main/index.ts
at Lazy (<anonymous>)
at RenderedRoute
at Outlet
...
Caused by: React ErrorBoundary TypeError: Failed to fetch dynamically imported module: ...
Caused by: React ErrorBoundary TypeError: Failed to fetch dynamically imported module: ...
Caused by: React ErrorBoundary TypeError: Failed to fetch dynamically imported module: ...
The Caused by block repeats once per Retry click, with an identical message every time.
What I found
React.lazy stores the rejection on its payload object and never calls the factory again. From the
React 19.2.5 development build:
function lazyInitializer(payload) {
if (-1 === payload._status) { // Uninitialized: the only path that calls the factory
var thenable = payload._result(); // () => import('...')
thenable.then(
function (moduleObject) { /* _status = 1 (Resolved) */ },
function (error) {
payload._status = 2; // Rejected
payload._result = error;
}
);
...
}
if (1 === payload._status) return payload._result.default;
throw payload._result; // Rejected: re-throw the stored error on every render
}
Since lazy() runs once at module scope, that payload lives as long as the document. Once
_status === 2, every later render throws the stored error without retrying the import. That
accounts for every symptom:
resetErrorBoundary()only re-renders the children;lazyInitializerthrows synchronously, so no request is made.- No thenable is thrown, so
Suspensenever engages and no loading state appears. - Payloads are per route, so unrelated routes are unaffected.
- Reloading the document is the only thing that discards the poisoned payload.
Constraint: I cannot reload the document
The usual advice for this error is window.location.reload(). Our native shell blocks page reloads,
so that is not available to me. I need recovery within the same document.
What I have already considered
1. Drop route-level code splitting
Static imports everywhere would remove the failure mode entirely, since navigation would fetch no code. The cost is initial bundle size and cold start: the build deliberately keeps heavy lazy-only dependencies (ECharts, framer-motion) out of the initial download, and this is a low-end-Android hybrid app where cold start matters.
2. Prefetch every route chunk after boot
Warm all chunks in the background once the app is running and online, so the module map is already populated before connectivity drops. This keeps the initial bundle small, but it is mitigation rather than recovery: a user who is offline when a prefetch fails ends up in the same state.
Question
Given that reloading the document is not an option, how do I make a failed React.lazy route
recoverable inside the same document?
Specifically:
- If the module map does cache the failure, is there a supported way to force a re-fetch of a Vite-resolved chunk without hardcoding or scraping its URL?
- Is there an established pattern for retryable lazy routes in a WebView app where reloading is not permitted?
I am looking for something that ends with a working Retry button, not just an approach that reduces how often the problem happens.