Prerequisites
Ionic Framework Version
v8.x (reproduced on 8.8.13; the unguarded code is still present on main and in 8.8.16-nightly.20260724)
Current Behavior
StackController.destroy() sets this.containerEl = undefined! (stack-controller.ts#L240). A transition that is still queued (via the wait() serialization) can resolve after the ion-router-outlet was destroyed — e.g. an interrupted, re-entrant, or cancelled navigation during fast back-navigation. When it does, transition() dereferences the now-undefined element:
const containerEl = this.containerEl; // undefined after destroy()
if ((containerEl as any).commit) { // TypeError
which throws:
TypeError: undefined is not an object (evaluating 'containerEl.commit')
at transition
We see this at meaningful scale in a production Ionic Angular + Capacitor app (Datadog RUM): several hundred crash events per week from a single app build, concentrated on fast back-navigation between nested router outlets (message-thread → channel list). The minified symbol varies per build (d.commit, Fn.commit, Tt.commit, Vn.commit), but all decode to this same line.
Expected Behavior
A transition that resolves after its outlet was destroyed should be a no-op (resolve false, the method's existing fallback) rather than throwing an unhandled TypeError.
Steps to Reproduce
The bug is a race between a queued transition and outlet destruction, so it is timing-dependent rather than deterministically reproducible in a minimal app:
- Navigate forward into a page hosted by a nested
ion-router-outlet.
- Trigger back-navigation twice in quick succession (double-tap back / rapid
NavController.back() calls) so that a second transition queues behind the first via StackController.wait().
- If the first navigation destroys the nested outlet (route leaves the outlet's subtree), the queued transition resolves against the destroyed stack controller and throws.
The code path makes the defect visible without a live repro: destroy() intentionally nulls containerEl (with a lint-suppressed non-null assertion), and transition() has no corresponding guard.
Code Reproduction URL
https://github.com/ionic-team/ionic-framework/blob/main/packages/angular/common/src/directives/navigation/stack-controller.ts#L240 (destroy) and https://github.com/ionic-team/ionic-framework/blob/main/packages/angular/common/src/directives/navigation/stack-controller.ts#L278 (unguarded dereference). A deterministic minimal repro is impractical for this race; production stack traces and the code path above demonstrate it. Happy to provide additional RUM data if useful.
Ionic Info
Ionic:
Ionic CLI : 7.2.1
Ionic Framework : @ionic/angular 8.8.13
@angular-devkit/build-angular : 21.2.19
@angular-devkit/schematics : 21.2.19
@angular/cli : 21.2.19
@ionic/angular-toolkit : 12.3.0
Capacitor:
Capacitor CLI : 8.4.1
@capacitor/android : 8.4.1
@capacitor/core : 8.4.1
@capacitor/ios : 8.4.1
System:
NodeJS : v24.18.0
npm : 11.16.0
OS : Windows 10
Additional Information
We have been shipping the one-line guard (if ((containerEl as any)?.commit)) via patch-package in production; the crash class disappears in patched builds with no observed side effects. PR with the fix incoming.
Prerequisites
Ionic Framework Version
v8.x (reproduced on 8.8.13; the unguarded code is still present on
mainand in8.8.16-nightly.20260724)Current Behavior
StackController.destroy()setsthis.containerEl = undefined!(stack-controller.ts#L240). A transition that is still queued (via thewait()serialization) can resolve after theion-router-outletwas destroyed — e.g. an interrupted, re-entrant, or cancelled navigation during fast back-navigation. When it does,transition()dereferences the now-undefinedelement:which throws:
We see this at meaningful scale in a production Ionic Angular + Capacitor app (Datadog RUM): several hundred crash events per week from a single app build, concentrated on fast back-navigation between nested router outlets (message-thread → channel list). The minified symbol varies per build (
d.commit,Fn.commit,Tt.commit,Vn.commit), but all decode to this same line.Expected Behavior
A transition that resolves after its outlet was destroyed should be a no-op (resolve
false, the method's existing fallback) rather than throwing an unhandledTypeError.Steps to Reproduce
The bug is a race between a queued transition and outlet destruction, so it is timing-dependent rather than deterministically reproducible in a minimal app:
ion-router-outlet.NavController.back()calls) so that a second transition queues behind the first viaStackController.wait().The code path makes the defect visible without a live repro:
destroy()intentionally nullscontainerEl(with a lint-suppressed non-null assertion), andtransition()has no corresponding guard.Code Reproduction URL
https://github.com/ionic-team/ionic-framework/blob/main/packages/angular/common/src/directives/navigation/stack-controller.ts#L240 (destroy) and https://github.com/ionic-team/ionic-framework/blob/main/packages/angular/common/src/directives/navigation/stack-controller.ts#L278 (unguarded dereference). A deterministic minimal repro is impractical for this race; production stack traces and the code path above demonstrate it. Happy to provide additional RUM data if useful.
Ionic Info
Additional Information
We have been shipping the one-line guard (
if ((containerEl as any)?.commit)) via patch-package in production; the crash class disappears in patched builds with no observed side effects. PR with the fix incoming.