If your app lazy-loads code with import(), you have probably seen this in your error tracker: Failed to fetch dynamically imported module. A user on a shaky train connection taps a button, the chunk request dies, and from that moment the feature is broken until they reload the page. Retrying in code did nothing, because the browser remembered the failure. Chrome 156, stable since 20 October 2026, changes that. A failed module fetch is no longer cached, so calling import() again makes a fresh request. Here is what changed, how to write a sensible retry, and where retrying will not save you.
Why retrying import() used to fail instantly
Every document keeps a module map: a record of each module URL it has tried to load and the result. Until now, the HTML spec said that when a module fetch failed, the map stored that failure permanently. The next import() of the same URL checked the map first, found the failure, and rejected immediately without touching the network.
The explainer for the change puts the consequence plainly: the only ways out were a full page reload or a cache-busting trick such as importing url + "#retry1", which the browser treats as a different module. Neither is something you want sprinkled through a codebase.
What changed in the spec and in Chrome 156
The fix is a small spec change with a long history. WHATWG pull request #10327, "Don't cache HTTP errors in the module map", was merged on 15 July 2026. Under the new rule, a fetch that fails with a network error, an HTTP error status or a MIME type mismatch is removed from the module map, so a later import of the same specifier fetches again.
Chrome shipped the behaviour in version 156. The Chrome 156 release notes describe it as letting developers retry a failed module load by calling import() again, which mostly helps people on unstable networks. There is no new API to learn. The only visible difference is that a second attempt now goes to the network.
A few details worth knowing:
Parse errors still stick. If the file arrives but is not valid JavaScript, the failure stays in the map. Fetching the same broken file again would not help anyway.
Parallel imports share one result. If two parts of your app import the same module while the first request is still in flight, both get the same outcome. Retries only help when they start after the failure.
Other browsers differ. According to the discussion on the pull request, Safari already re-fetches for sequential imports after a failure, and a Firefox bug has been filed. Treat "retry works" as true in Chrome 156+ and Safari, and unconfirmed elsewhere for now (as of October 2026).
A retry helper you can drop in
The helper below wraps any loader function and retries with a growing delay. It takes a function, not a URL, so bundlers can still see the literal import('./chart.js') and split the chunk properly.
// retry-import.js
export async function importWithRetry(load, { retries = 2, baseDelay = 500 } = {}) {
for (let attempt = 0; ; attempt++) {
try {
return await load();
} catch (error) {
if (attempt >= retries) throw error;
const delay = baseDelay * 2 ** attempt; // 500 ms, then 1000 ms
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
Using it for a feature that loads on click:
import { importWithRetry } from './retry-import.js';
button.addEventListener('click', async () => {
try {
const { renderChart } = await importWithRetry(() => import('./chart.js'));
renderChart(container);
} catch {
showMessage('Could not load the chart. Check your connection and try again.');
}
});
And with React.lazy, which expects a function that returns a promise for the module:
import { lazy } from 'react';
import { importWithRetry } from './retry-import.js';
const Chart = lazy(() => importWithRetry(() => import('./Chart.jsx')));
Two retries with a short backoff is a reasonable default. A user waiting on a button press will not sit through ten attempts, and hammering a struggling network rarely helps. If you already use lazy routes in a framework, the same idea applies to its loader functions; if you are moving between major versions, our walkthrough on upgrading to Next.js 16 covers where those loaders live now.
What happens in older browsers
Nothing breaks. In a browser that still caches the failure, each retry rejects immediately, the loop finishes in about a second and a half, and your catch block shows the error message. You lose nothing by shipping the helper today. If you need retries to work everywhere right now, some teams add a query string on the final attempt so the browser sees a new URL. Be careful with that: a different URL is a different module, so any state the module holds will not be shared with another copy loaded under the original URL.
When a retry will not help: deploy skew
Many "failed to fetch dynamically imported module" errors are not network blips at all. You deploy a new build, the old hashed chunk files are deleted from the server, and a user who opened the page an hour ago clicks a link that asks for chunk-3f9a1c.js. The server returns 404. Under the new rule the browser will happily fetch again, and get 404 again.
Retrying cannot fix a file that no longer exists. For this case you need a different response:
Keep old assets around for a while after each deploy, so open tabs can still load the chunks they reference.
Detect a stale client and reload. After the retries are exhausted, a reasonable fallback is to check whether a new version is live (for example by fetching a small version file) and, if so, do a full reload once, guarded so it cannot loop.
Log which error you got. Network failures and 404s look the same to
import(), so record the chunk URL and check your server logs to see which one you are dealing with.
Should you change anything today?
If you lazy-load anything that matters, such as a checkout step, an editor or a settings panel, wrap those imports in a retry helper now. It costs a few lines, it degrades gracefully in browsers that have not shipped the change, and in Chrome 156 and later it turns a dead feature into a short pause for users on bad connections.
Then look at your error tracker and split the failures by status. If most are 404s after deploys, the fix belongs in your hosting setup, not in your retry loop. And if you are curious what else just became safe to use across browsers, the recent CSS features that reached Baseline are worth a look while you are in the release notes.



