Skip to content

Web: loadScript double-completes its Completer — 'Bad state: Future already completed' reported as fatal #3906

Description

@vekaidnie

Platform

Flutter Web (dart2js release build)

SDK version

sentry_flutter 9.14.0 — but the affected code is unchanged in 9.24.0:
https://github.com/getsentry/sentry-dart/blob/9.24.0/packages/flutter/lib/src/web/script_loader/web_script_dom_api.dart

Description

loadScript() wires both listeners straight to the completer with no isCompleted guards:

final completer = Completer<void>();
final script = HTMLScriptElement()
  ..crossOrigin = 'anonymous'
  ..onLoad.listen((_) => completer.complete())
  ..onError.listen((event) => completer.completeError('Failed to load $src'));

onLoad/onError are streams, and browsers can deliver more than one event for the same <script> element — e.g. an error during a flaky mobile connection followed by a later load, or event re-delivery around Safari back/forward-cache restores. The second delivery throws StateError: Bad state: Future already completed inside the DOM event callback, which surfaces as an unhandled fatal event in Sentry itself — the SDK reports its own loader crash.

We see this steadily in production on a public Flutter-web page opened mostly from mobile devices (iOS Safari heavy): 11 events / 9 users over ~10 weeks, level fatal, culprit web_script_dom_api.dart loadScript.<anonymous function>. The page itself is unaffected — it's pure noise, but fatal-level noise that pollutes release health and triggers regression alerts on every release.

Expected

The loader should complete at most once, e.g.:

..onLoad.listen((_) {
  if (!completer.isCompleted) completer.complete();
})
..onError.listen((event) {
  if (!completer.isCompleted) completer.completeError('Failed to load $src');
});

(or .first-based single-shot subscriptions that cancel the sibling listener).

Steps to reproduce

Hard to reproduce deterministically — it needs a double load/error event delivery on the injected SDK <script>. Consistently observable in aggregate on any high-traffic mobile-web deployment with SentryFlutter.init defaults (JS SDK auto-loading enabled).

🤖 Filed with Claude Code

Metadata

Metadata

Assignees

Projects

Status
No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions