Skip to content

ɵzoneWrap ignores a handler passed as an observer object, and holds a wrapped promise forever once its injector is destroyed #3828

Open

Description

@armando-navarro

Version info

Angular: 22.2

Firebase: 12.19.0

AngularFire: 22.0.0-rc.0, and main

Description

Two defects in ɵzoneWrap (src/zones.ts), the function the generated @angular/fire/* wrappers go through.

1. A handler passed as an object is not wrapped. Firebase accepts a subscription handler either as a function, onSnapshot(ref, snapshot => ...), or as an observer object, onSnapshot(ref, { next: snapshot => ... }). The wrapper rewrites an argument only when typeof is 'function', so the object form reaches the SDK untouched:

  • No pending task is registered, so server rendering does not wait for the first value.
  • inject() throws inside the handler, because nothing re-enters the injection context.
  • In a zone.js app the handler runs outside the Angular zone, because the wrapper calls Firebase inside runOutsideAngular and then never brings the object's members back in. The unwrapped Firebase call made from in-zone code keeps its handler in the zone, so the wrapper makes the object form worse than not wrapping at all.

Eleven wrapped functions accept the object form: onAuthStateChanged, onIdTokenChanged, onTokenChanged, onSnapshot, onSnapshotResume, onSnapshotsInSync, onMessage, onRegistered, onUnregistered, onConfigUpdate (which accepts only the object form) and data-connect's subscribe (whose object uses onNext, onErr and onComplete). The three messaging functions are generated with blockUntilFirst: false, so the pending task point does not apply to them.

2. A wrapped promise never settles after its injector is destroyed.

  • When a wrapped call returns a Promise, the wrapper resolves its own promise through runInInjectionContext(injector, () => run(() => resolve(value))).
  • Neither layer reaches the caller: a .then callback runs as a later microtask after the injection context is torn down, and zone.js runs it in the zone where .then was attached, not where resolve was called.
  • The one thing the layers do is throw: runInInjectionContext checks assertNotDestroyed, so when the Firebase promise settles after ApplicationRef.destroy() or a TestBed reset, the callback throws NG0205 before resolve runs and the caller's promise never settles.

How to reproduce these conditions

On a zone.js Angular 22.2 app and a zoneless one, against the Firestore emulator, call the real functions through ɵzoneWrap the way a constructor does, inside the Angular zone and an injection context, and record inside the handler whether a pending task was registered, whether inject() works and whether NgZone.isInAngularZone() is true:

  • onSnapshot(ref, { next, error }) and onSnapshotsInSync(firestore, { next, error }): no task in either app, inject() throws in both, and in the zone.js app the handler runs outside the Angular zone. A plain property written from the handler does not render there, in a component with default change detection.
  • onSnapshot(ref, next), the function form: one task, released when the handler ran, inject() works, and in the zone.js app the handler runs inside the zone.
  • The unwrapped onSnapshot with either form of handler, called from the same place: inject() throws, but in the zone.js app the handler stays inside the zone.

For the promise defect, a spec: wrap a function that resolves after 20 ms with ɵzoneWrap(fn, true), call it inside TestBed.runInInjectionContext, call TestBed.resetTestingModule(), then await the returned promise. It never settles and the spec times out after 5 seconds.

Expected behavior

A handler passed as an observer object gets what a handler passed as a function gets: the pending task, the Angular zone and a working inject(). A wrapped promise settles whenever the Firebase promise settles.

Actual behavior

The object form gets none of the three, and a wrapped promise that settles after its injector is destroyed never settles.

Activity

  1. added
    comp: coreFirebaseApp DI core (src/app).
    comp: firestoreCloud Firestore (src/firestore).
    comp: zonesChange detection / zone.js / zoneless.
    type: bugDefect: expected behavior doesn't happen.
    on Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    comp: app-checkApp Check (src/app-check).comp: coreFirebaseApp DI core (src/app).comp: firestoreCloud Firestore (src/firestore).comp: remote-configRemote Config (src/remote-config).comp: zonesChange detection / zone.js / zoneless.type: bugDefect: expected behavior doesn't happen.version: current (v17+)Targets the current modular API (v17+).

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions