Astro Info
Astro v5.18.1
Vite v7.0.3
Node v24.14.1
System Linux (x64)
Package Manager npm
Output server
Adapter @astrojs/node (v9.5.5)
Integrations @astrojs/react (v4.4.2)
set-prerender
If this issue only occurs in one browser, which browser is a problem?
All Browsers
Describe the Bug
When a network request for an island's component-url or renderer-url fails during hydration, the astro-island custom element catches the error in start() and only calls console.error. The rejected promise from the client:* directive callback then surfaces as an unhandled promise rejection at the window level. There is no retry, no custom event dispatched, and no way for application code to detect or recover from the failure.
When using Sentry, those errors pop up as Failed to fetch dynamically imported module (Chrome), error loading dynamically imported module (Firefox) or Importing a module script failed.
There are several problems with that:
1. No error event for application code to hook into
Vite for example provides vite:preloadError for its preload helper failures. There is no equivalent astro:hydration-error (or similar) event for the island hydration path. Application code cannot detect that an island failed to hydrate and respond (e.g. show a fallback, retry, or report to an error tracking service with structured context).
2. No retry on transient failures
A single failed import() permanently breaks the island. A retry with a short delay would recover from transient network issues (DNS hiccups, mobile connection drops, carrier proxy interference) without requiring a full page reload.
3. One network hiccup produces N unhandled rejections
A page with 20 islands that all load from the same CDN origin will produce 20 separate unhandled promise rejections if that origin is momentarily unreachable. Each island fails independently with no coordination or deduplication.
Real-world impact
We operate a high-traffic Astro site with ~20 islands per page, assets served from a CDN. We see 100k Sentry events from 15k users per week from this issue. Examining individual cases shows the pattern clearly:
- One user loads one page → most of the islands fail at the exact same second (same
trace_id, same timestamp)
- Each failed island produces a separate unhandled rejection → 24 Sentry events from one page load
- The user is left with a fully rendered SSR page where no interactive element works
The affected users are concentrated in regions with less reliable mobile connectivity, but the network metrics reported by the browser look healthy — suggesting brief DNS failures or carrier-level interference rather than sustained poor connectivity.
The relevant code path
In the prebuilt astro-island custom element, start() does:
async start() {
// ...
try {
await Astro[client](async () => {
const [h, { default: p }] = await Promise.all([
import(this.getAttribute("component-url")),
import(renderer_url)
]);
// ...
return this.hydrate;
}, opts, this);
} catch (n) {
console.error(`[astro-island] Error hydrating ${url}`, n);
// ← no retry, no event dispatch, island is permanently broken
}
}
Compare this to Vite's __vitePreload which dispatches vite:preloadError and allows event.preventDefault() to suppress the throw. The island hydration path has no equivalent.
What's the expected result?
-
An event should be dispatched when island hydration fails, e.g. astro:hydration-error on the astro-island element or on window, with the error and component URL in the event detail. This lets application code detect failures and respond (show fallback UI, report to error tracking, trigger a reload).
-
The import() could be retried at least once before giving up. A single retry with a ~1s delay would recover from most transient network failures without any application-side code. Maybe this could be configurable.
-
Ideally, the error should not surface as an unhandled promise rejection. The current behavior where the catch block only console.errors but the directive callback's rejected promise bubbles up as unhandled is confusing — it looks like an application bug rather than a network issue. At least the error should have a common exception message and not separate ones per browser.
Link to Minimal Reproducible Example
The issue requires real network failures to reproduce. It cannot be triggered in a local dev environment. To simulate: deploy an Astro site with multiple client:load islands loading from a CDN, then use Chrome DevTools → Network → Request Blocking to block the CDN domain and reload the page. All islands will fail silently with only console.error output.
Participation
Astro Info
If this issue only occurs in one browser, which browser is a problem?
All Browsers
Describe the Bug
When a network request for an island's
component-urlorrenderer-urlfails during hydration, theastro-islandcustom element catches the error instart()and only callsconsole.error. The rejected promise from theclient:*directive callback then surfaces as an unhandled promise rejection at the window level. There is no retry, no custom event dispatched, and no way for application code to detect or recover from the failure.When using Sentry, those errors pop up as
Failed to fetch dynamically imported module(Chrome),error loading dynamically imported module(Firefox) orImporting a module script failed.There are several problems with that:
1. No error event for application code to hook into
Vite for example provides
vite:preloadErrorfor its preload helper failures. There is no equivalentastro:hydration-error(or similar) event for the island hydration path. Application code cannot detect that an island failed to hydrate and respond (e.g. show a fallback, retry, or report to an error tracking service with structured context).2. No retry on transient failures
A single failed
import()permanently breaks the island. A retry with a short delay would recover from transient network issues (DNS hiccups, mobile connection drops, carrier proxy interference) without requiring a full page reload.3. One network hiccup produces N unhandled rejections
A page with 20 islands that all load from the same CDN origin will produce 20 separate unhandled promise rejections if that origin is momentarily unreachable. Each island fails independently with no coordination or deduplication.
Real-world impact
We operate a high-traffic Astro site with ~20 islands per page, assets served from a CDN. We see 100k Sentry events from 15k users per week from this issue. Examining individual cases shows the pattern clearly:
trace_id, same timestamp)The affected users are concentrated in regions with less reliable mobile connectivity, but the network metrics reported by the browser look healthy — suggesting brief DNS failures or carrier-level interference rather than sustained poor connectivity.
The relevant code path
In the prebuilt
astro-islandcustom element,start()does:Compare this to Vite's
__vitePreloadwhich dispatchesvite:preloadErrorand allowsevent.preventDefault()to suppress the throw. The island hydration path has no equivalent.What's the expected result?
An event should be dispatched when island hydration fails, e.g.
astro:hydration-erroron theastro-islandelement or onwindow, with the error and component URL in the event detail. This lets application code detect failures and respond (show fallback UI, report to error tracking, trigger a reload).The
import()could be retried at least once before giving up. A single retry with a ~1s delay would recover from most transient network failures without any application-side code. Maybe this could be configurable.Ideally, the error should not surface as an unhandled promise rejection. The current behavior where the
catchblock onlyconsole.errors but the directive callback's rejected promise bubbles up as unhandled is confusing — it looks like an application bug rather than a network issue. At least the error should have a common exception message and not separate ones per browser.Link to Minimal Reproducible Example
The issue requires real network failures to reproduce. It cannot be triggered in a local dev environment. To simulate: deploy an Astro site with multiple
client:loadislands loading from a CDN, then use Chrome DevTools → Network → Request Blocking to block the CDN domain and reload the page. All islands will fail silently with onlyconsole.erroroutput.Participation