Skip to content

SvelteKit: stale-tab 'Failed to fetch dynamically imported module' still reported after a deploy despite version polling

SvelteKit 2.x app (adapter-node, Vite) deployed several times a day. Sentry keeps getting TypeError: Failed to fetch dynamically imported module: https://example.com/_app/immutable/nodes/15.abc123.js (Chrome) / Importing a module script failed (Safari) from tabs opened before a deploy. Added kit.version.pollInterval plus a beforeNavigate hook that does location.href = to.url.href when $updated is true, as the docs suggest, but events continue. The error reaches handleError with mechanism auto.function.sveltekit.handle_error, or arrives as an unhandled rejection with no breadcrumbs.

1 solution
ranked by outcome — not votes
Accepted

SvelteKit already recovers from a failed route import during a real navigation: the client runs updated.check() and does a native reload before calling handleError (kit 2.70.3 client.js ~1354-1359). The events that still reach Sentry come from paths that never fire beforeNavigate:

  1. Preloads. With data-sveltekit-preload-data="hover" (the default template), hovering/touching a link preloads the route; a failed import there goes straight to handleError without the version check (client.js ~1333-1335).
  2. Lazy imports outside the router, e.g. await import('posthog-js') in onMount: an unhandled rejection, no navigation at all.

Keep the beforeNavigate reload (it does keep link/goto/form navigations fresh; call cancel() before setting location.href, and skip type === 'popstate', where cancel() would history.go() back and a full load aborts the in-flight __data.json into a new TypeError: network error). Then filter stale-build noise in Sentry.init({ beforeSend }), which may return a Promise:

import { version } from '$app/environment';
const CHUNK = /Failed to fetch dynamically imported module|Importing a module script failed|error loading dynamically imported module|Unable to preload CSS for/;
beforeSend: (event) => event.exception?.values?.some((e) => CHUNK.test(e.value ?? ''))
  ? fetch('/_app/version.json', { cache: 'no-store' })
      .then((r) => r.json()).then((j) => (j.version !== version ? null : event))
      .catch(() => event)
  : event,

This drops the event only when the server's build differs from the tab's; a failed import on the current build is a real bug and still reports. Also drop _app/version.json fetch breadcrumbs in beforeBreadcrumb, or the 60 s poll fills every trail.