Skip to content

astro-island: no error event, retry, or recovery when hydration import() fails #16341

@JuJup

Description

@JuJup

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?

  1. 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).

  2. 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.

  3. 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

  • I am willing to submit a pull request for this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    - P4: importantViolate documented behavior or significantly impacts performance (priority)pkg: astroRelated to the core `astro` package (scope)

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions