Failed to fetch dynamically imported module in React

Matthew C.
—The Problem
When lazy loading components using React Suspense, you may encounter the following error:
Uncaught TypeError: Failed to fetch dynamically imported module: https://example.com/assets/Home-d165e21c.js
You may also get a ChunkLoad error:
Unhandled Runtime Error ChunkLoadError: Loading chunk _app-pages-browser_src_app_Home_tsx failed.
The React lazy function lazy-loads components only as they’re needed to improve performance.
For example, you may have a page that shows a complex map that relies on a large third-party library. You can use lazy loading to show the map and fetch its dependencies only if the user chooses to see the map by clicking a Show map button:
import { Suspense, lazy, useState } from "react"; import Info from "./Info"; const Map = lazy(() => import("./Map")); export default function Directions() { const [showMap, setShowMap] = useState(false); return ( <div> <Info /> <button onClick={() => setShowMap(!showMap)}> {showMap ? "Hide map" : "Show map"} </button> {showMap ? ( <Suspense fallback={<div>Loading...</div>}> <Map /> </Suspense> ) : ( "" )} </div> ); }
The lazy function takes in a load argument and returns a Promise or another thenable. The load function must resolve to an object whose .default property is a valid React component type.
If the lazy-loaded component can’t be downloaded, you’ll get one of the above errors. This may occur due to a network issue such as a slow or intermittent internet connection.
Another common cause is a cache issue after deploying a new version of your app. React apps often use a bundler such as Vite or Webpack that adds a hash to the file names of assets. Assets include HTML, CSS, and JavaScript files. When a change is made to an asset, such as the JavaScript file for a module that is lazily imported, the file name hash changes. The hashes are added to change the file names each time the app is updated so that the browser does not use cached versions of the assets. This technique of changing an asset’s name, and subsequently its URL, is known as cache-busting.
However, if a user accesses your website before your app is updated, stays online during the update, and triggers lazy loading of the component file, the component import may fail. This may occur because the user has a browser-cached version of the page that has a URL for the old version of the React component file to be lazy-loaded that no longer exists.
The Solution
To handle network issues, you can create a wrapper function for the React lazy function that retries the import. The problem with just retrying the import is that the first import is cached, even if it’s a failed import. The retries would return the cached failed response. This module fetching behavior, where HTTP errors are cached, may change.
A basic solution to this issue is to catch chunk loading errors with a React error boundary component. A chunk error occurs if an application encounters issues loading a JavaScript chunk (bundle). You can use a React error boundary component to catch chunk loading errors and display a message to the user with a page refresh button:
class ChunkErrorBoundary extends React.Component { state = { hasError: false }; static getDerivedStateFromError(error) { // Only show refresh for chunk load errors if ( error.name === "ChunkLoadError" || error.message.includes("Failed to fetch dynamically imported module") ) { return { hasError: true }; } return { hasError: false }; } render() { if (this.state.hasError) { return ( <div> <p>Failed to load component.</p> <button onClick={() => window.location.reload()} > Refresh page </button> </div> ); } return this.props.children; } }
Wrap your suspense boundary with the ChunkErrorBoundary component:
<ChunkErrorBoundary> <Suspense fallback={<div>Loading...</div>}> <Map /> </Suspense> </ChunkErrorBoundary>
If the user clicks the Show map button and the Map component import fails, the user can click the Refresh page button to refresh the page. The next time the user clicks the Show map button, a fresh attempt to import the module will be made.
If you’re using Next.js with the App Router, you can easily create an error boundary by adding an error.tsx file inside a route segment and exporting an error component from it as explained in the Next.js docs.
Other possible, but more complex, solutions include creating versioned deployments and notifying users to refresh the application when a new version is detected. New versions can be detected by using a version number stored in an environment variable and added to URLs. Another way to detect a version change is to use a service worker.
- Sentry BlogGuide to Error & Exception Handling in React (opens in a new tab)
- Sentry BlogHow to identify fetch waterfalls in React (opens in a new tab)
- Syntax.fmReact Server Components (opens in a new tab)
- Sentry BlogSentry can’t fix React hydration errors, but it can really help you debug them (opens in a new tab)
- Syntax.fmWhy the jQuery Creator Uses React and Typescript (opens in a new tab)
- Syntax.fmListen to the Syntax Podcast (opens in a new tab)
- Sentry BlogReact Native Debugging and Error Tracking During App Development (opens in a new tab)
- Syntax.fmDiscussion on building native iOS and Android apps with React Native (opens in a new tab)
- SentryReact Error & Performance Monitoring (opens in a new tab)
- Sentry BlogFixing memoization-breaking re-renders in React (opens in a new tab)
- SentryReact Debug Hub (opens in a new tab)
- Listen to the Syntax Podcast (opens in a new tab)
![Syntax.fm logo]()
Tasty treats for web developers brought to you by Sentry. Get tips and tricks from Wes Bos and Scott Tolinski.
SEE EPISODES
Considered “not bad” by 4 million developers and more than 150,000 organizations worldwide, Sentry provides code-level observability to many of the world’s best-known companies like Disney, Peloton, Cloudflare, Eventbrite, Slack, Supercell, and Rockstar Games. Each month we process billions of exceptions from the most popular products on the internet.
