"Failed to fetch dynamically imported module" almost always happens to a real user, at a genuinely bad moment: they've had your app open in a tab since before your last deployment, and their browser just tried to lazy-load a JavaScript chunk that no longer exists at that URL because your build replaced it with a new, differently-hashed file.
This error means a dynamic import() call (used for code-splitting, lazy-loaded routes, or on-demand feature loading) failed to fetch the JavaScript file it needed — most commonly because the file's URL, which typically includes a content hash, changed between when the user's page first loaded and when they triggered the dynamic import, since your deployment replaced the old build's assets with a new one.
Why This Error Happens
Modern bundlers split code into separate chunk files, each named with a content hash for cache-busting (chunk-a3f8b2.js). When you deploy a new build, the old chunk files are typically removed from your hosting/CDN and replaced with newly-hashed ones. A user who loaded your app before the deployment has JavaScript in memory referencing the old chunk URLs — if they navigate to a route or trigger a feature that wasn't loaded yet, requiring a dynamic import of an old-hashed chunk that no longer exists, the fetch fails with this error.
Reproducing the Error
The deployment-timing scenario that causes this in practice:
// Route lazily loaded via dynamic import
const SettingsPage = lazy(() => import("./pages/SettingsPage"));
// Bundled as e.g. /assets/SettingsPage-a3f8b2.js
// 1. User loads the app; index page loaded, SettingsPage chunk not yet fetched
// 2. You deploy a new build; old chunk files removed, new ones with different hashes added
// 3. User clicks "Settings" in the still-open tab, triggering the dynamic import
// 4. Browser requests /assets/SettingsPage-a3f8b2.js — 404, file no longer exists
// Error: Failed to fetch dynamically imported module
Core Concepts Behind This Error
This is fundamentally a stale-client problem, not a code bug — the user's JavaScript bundle in memory is simply out of date relative to your currently-deployed assets, and no amount of fixing the import statement itself addresses the root cause, which is the mismatch between an old client and a new deployment.
A global error handler catching this specific error and prompting a page reload is the standard, pragmatic mitigation — since the actual fix (the user having the current build) requires a fresh page load, catching the error and triggering that reload (ideally with a user-friendly prompt rather than a silent forced reload) is the practical response most production apps use.
Some hosting platforms and CDN configurations retain old build assets for a grace period specifically to reduce this error's frequency — keeping recently-superseded chunk files available for some time after a new deployment gives already-loaded clients a window to still fetch what they need before those files are eventually cleaned up.
This error is distinct from a genuine 404 due to a broken build or misconfigured asset path — the deployment-timing version resolves itself on page refresh; a persistent version across all users regardless of when they loaded the page indicates an actual build or hosting configuration problem instead.
Fixing "Failed to Fetch Dynamically Imported Module"
Fix 1: Catch the error globally and prompt the user to reload, rather than letting it surface as an unhandled, confusing failure:
window.addEventListener("vite:preloadError", (event) => {
// Vite-specific event for this exact scenario
window.location.reload();
});
// Framework-agnostic version around a dynamic import call site
async function safeLazyImport<T>(importer: () => Promise<T>): Promise<T> {
try {
return await importer();
} catch (err) {
if (String(err).includes("Failed to fetch dynamically imported module")) {
window.location.reload();
}
throw err;
}
}
Fix 2: Configure your hosting/CDN to retain recently-superseded build assets for a grace period rather than deleting them immediately on deploy, reducing the window in which this error can occur:
# Example: keep the last 2-3 deployments' assets available
# (specific configuration depends on your hosting platform)
Fix 3: Implement an app-level "new version available" banner that detects a new deployment (via a version check or polling a version endpoint) and prompts the user to refresh proactively, before they hit a broken dynamic import at all:
useEffect(() => {
const interval = setInterval(async () => {
const { version } = await fetch("/api/version").then((r) => r.json());
if (version !== currentBuildVersion) {
showUpdateBanner();
}
}, 5 * 60 * 1000);
return () => clearInterval(interval);
}, []);
Should You Just Auto-Reload on This Error Without Prompting the User?
For most applications, prompt rather than silently force-reload — an unannounced page reload can lose unsaved user input or work in progress, which is a worse experience than the original error in some cases. Reserve silent auto-reload for applications where losing in-progress state genuinely isn't a concern (a mostly-read-only dashboard, for example), and prefer a visible "Update available, click to refresh" prompt for anything with meaningful user input state.
Preventing This Error in Production
Retain recently-superseded build assets on your CDN/hosting for a grace period after each deployment, reducing how often already-loaded clients hit a genuinely missing chunk file. Add a global handler catching this specific error pattern and prompting (or gracefully triggering) a reload, since some frequency of this error is close to unavoidable for any actively-deployed single-page application with code splitting.
If you're seeing this error in production error tracking, check whether it clusters immediately after deployments — that pattern confirms it's the expected stale-client scenario, and the fix is graceful handling, not chasing a code-level bug that doesn't exist.