What the error means
Network errors reach your code in different ways. fetch() rejects with a TypeError. Axios rejects with an AxiosError that has code ERR_NETWORK. XMLHttpRequest fires an error event, and its status is 0.
A bundler that loads code on demand fails the same way when a chunk file cannot be downloaded. After a deploy, users on the old version ask for chunk files that no longer exist.
| Where | Message |
|---|---|
| fetch in Chrome, Edge | TypeError: Failed to fetch |
| fetch in Firefox | TypeError: NetworkError when attempting to fetch resource. |
| fetch in Safari | TypeError: Load failed |
| Axios | AxiosError: Network Error |
| webpack lazy chunk | ChunkLoadError: Loading chunk 42 failed. |
| Vite dynamic import in Chrome | TypeError: Failed to fetch dynamically imported module |
Common causes
- The device is offline, or it changed networks during the request, for example from Wi-Fi to cellular.
- DNS, TLS, or the connection failed: the host is down, the name is wrong, or the certificate is not valid.
- The browser blocked the request: CORS, mixed content, a Content Security Policy connect-src rule, or an extension.
- A deploy removed old chunk files while users still ran the previous version of the app.
- Your own timeout cancelled the request. That is an AbortError or TimeoutError, not a network error, and it needs its own handling.
Reproduce it
In DevTools, open Network and set throttling to Offline. Then run a request. Use Block request URL on a chunk file to see a chunk load error.
try { await fetch("/api/orders");} catch (error) { console.log(error); // TypeError: Failed to fetch} console.log(navigator.onLine); // falseFix it
Set a timeout, and handle it separately
AbortSignal.timeout() cancels a slow request with a TimeoutError. Show a slow-server message for it, and an offline message for a real network error.
TypeScripttry { const response = await fetch("/api/orders", { signal: AbortSignal.timeout(8000), });} catch (error) { if (error instanceof DOMException && error.name === "TimeoutError") { showMessage("The server is slow. Try again."); } else { showMessage("You appear to be offline."); }}Wait for the connection before you retry
Retry reads that are safe to repeat after the browser is back online. navigator.onLine can say true on a network that has no internet, so keep a retry limit.
TypeScriptasync function whenOnline() { if (navigator.onLine) return; await new Promise((resolve) => window.addEventListener("online", resolve, { once: true }), );}Reload once when a chunk is gone after a deploy
When a lazy import fails, load the new version of the app one time. Store a flag so a real outage does not cause a reload loop.
TypeScriptasync function loadSettings() { try { return await import("./settings-page"); } catch (error) { if (!sessionStorage.getItem("reloaded-for-deploy")) { sessionStorage.setItem("reloaded-for-deploy", "1"); window.location.reload(); } throw error; }}
Catch it in production
The Squasher browser SDK sends this error to your project when it happens, so you do not wait for a user to report it.
- Uncaught rejections from fetch and dynamic imports are captured automatically, with the page URL, release, and session ID.
- The release on each event shows when chunk errors start right after a deploy.
- Session replay records failed network requests next to what the user saw.
import { init, captureError } from "@squasher-ai/browser"; init({ apiKey: import.meta.env.VITE_SQUASHER_API_KEY, projectId: import.meta.env.VITE_SQUASHER_PROJECT_ID, release: import.meta.env.VITE_GIT_SHA,}); export async function getOrders() { try { return await fetch("/api/orders", { signal: AbortSignal.timeout(8000) }); } catch (error) { captureError(error as Error, { endpoint: "/api/orders" }); throw error; }}TypeError: Failed to fetch dynamically imported module
UnhandledRejection(TypeError)
- Events
- 640
- First seen
- 38m ago
- Last seen
- now
- Environment
- production
- Service
- shop-web
Example code and issue.


