Which @angular/* package(s) are the source of the bug?
router
Is this a regression?
No
Description
In production, Safari 17.6 users sporadically click a link and the navigation neither completes nor fails: no NavigationEnd, no NavigationError, and the promise returned by router.navigate() never settles. Instead, the app's ErrorHandler receives the router's internal NoMatch (empty message, segmentGroup property) through NgZone.onError, reported by zone.js as an unhandled rejection.
The app uses zone.js 0.15.1 with provideZoneChangeDetection() and is built with @angular/build, so async/await is lowered to generators with esbuild's __async helper. Mapped against the production chunks, the stack reads:
| Frame |
Code |
[email protected]:1:38159 |
NoMatch constructor |
@chunk-FFUPTH3R.js:1:45633 |
throw new NoMatch(rawSegment) in Recognizer.matchSegmentAgainstRoute |
next@[native code] |
generator resume |
[email protected]:1:1425 |
esbuild __async step: o = f => { try { i(c.next(f)) } catch (j) { e(j) } } |
@polyfills.js:2:562 |
zone.js wrapper of a then callback, whose catch rejects the chained promise |
Our reading:
- The generator of
matchSegmentAgainstRoute resumes after await firstValueFrom(matchWithChecks(…)) and throws NoMatch, as designed.
- The
try/catch in o does not catch it, although it surrounds the c.next(f) call that threw. JavaScript semantics rule this out, so we suspect a defect in Safari 17.6's JavaScriptCore.
- zone.js catches the exception in its
then wrapper, rejects the promise from Promise.resolve(value).then(o, p), which nobody observes, and reports it as an unhandled rejection. zone.js 0.15 passes the original value on.
- The promise of
matchSegmentAgainstRoute never settles, so processSegment waits forever and the navigation hangs until the next navigation supersedes it.
It shows in the router because the recognizer signals "this route does not match" by throwing NoMatch and catching it in processSegment. With many sibling routes in named outlets (126 children in our case), one navigation throws up to about 70 exceptions through async functions, by far the most in the application. The failure mode depends on the recognize stage using async/await (#62994). #70900 mentions NoMatch and AbsoluteRedirect escaping in production; this may be the same thing.
Suggestion: signal "no match" with a return value instead of an exception. For example, processSegmentAgainstRoute and matchSegmentAgainstRoute could resolve to a sentinel that processSegment checks before it tries the next route. That takes the exception flow through async/await out of every navigation, whatever the engine does, and saves creating dozens of Error objects with stack traces per navigation.
Please provide a link to a minimal reproduction of the bug
Not reproduced outside Safari 17.6 yet, see "Anything else?".
Please provide the exception or error you saw
Error
t@https://<host>/chunk-FFUPTH3R.js:1:38159
@https://<host>/chunk-FFUPTH3R.js:1:45633
next@[native code]
o@https://<host>/chunk-KFP6Q3NT.js:1:1425
onInvoke@https://<host>/chunk-GR63O44E.js:4:19837
run@https://<host>/polyfills-P23JZYKH.js:1:7847
@https://<host>/polyfills-P23JZYKH.js:2:562
onInvokeTask@https://<host>/chunk-GR63O44E.js:4:19655
runTask@https://<host>/polyfills-P23JZYKH.js:1:8496
Y@https://<host>/polyfills-P23JZYKH.js:1:15714
invokeTask@https://<host>/polyfills-P23JZYKH.js:1:14763
User agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15
Please provide the environment you discovered this bug in (run ng version)
@angular/core, @angular/router: 21.2.12
@angular/build, @angular/cli: 21.2.10
zone.js: 0.15.1
TypeScript: 5.9
Browser: Safari 17.6 (macOS)
Angular CLI : 21.2.10
Angular : 21.2.12
Node.js : 24.16.0
Package Manager : npm 11.13.0
Operating System : win32 x64
┌──────────────────────────────────┬───────────────────┬───────────────────┐
│ Package │ Installed Version │ Requested Version │
├──────────────────────────────────┼───────────────────┼───────────────────┤
│ @angular/build │ 21.2.10 │ ^21.2.10 │
│ @angular/cdk │ 21.2.10 │ ^21.2.10 │
│ @angular/cli │ 21.2.10 │ ^21.2.10 │
│ @angular/common │ 21.2.12 │ ^21.2.12 │
│ @angular/compiler │ 21.2.12 │ ^21.2.12 │
│ @angular/compiler-cli │ 21.2.12 │ ^21.2.12 │
│ @angular/core │ 21.2.12 │ ^21.2.12 │
│ @angular/elements │ 21.2.12 │ ^21.2.12 │
│ @angular/forms │ 21.2.12 │ ^21.2.12 │
│ @angular/language-service │ 21.2.12 │ ^21.2.12 │
│ @angular/localize │ 21.2.12 │ ^21.2.12 │
│ @angular/material │ 21.2.10 │ ^21.2.10 │
│ @angular/material-moment-adapter │ 21.2.10 │ ^21.2.10 │
│ @angular/platform-browser │ 21.2.12 │ ^21.2.12 │
│ @angular/router │ 21.2.12 │ ^21.2.12 │
│ rxjs │ 7.8.2 │ ^7.8.2 │
│ typescript │ 5.9.3 │ ^5.9.2 │
│ zone.js │ 0.15.1 │ ^0.15.0 │
└──────────────────────────────────┴───────────────────┴───────────────────┘
Anything else?
Reproduction attempts: the production chunks, run against our route configuration in JavaScriptCore via Bun 1.0.0 to 1.3.5 (20,000 to 40,000 recognitions each, plain and inside NgZone, with aggressive JIT settings), never failed. In the field the failures are sporadic and sit between successful navigations to the same URLs.
Workaround in our app, in case it helps others: the ErrorHandler recognizes the escaped NoMatch ('segmentGroup' in error) and restarts router.currentNavigation() while its finalUrl is unset, in a setTimeout because zone.js reports unhandled rejections after draining the microtask queue. We also moved the most used routes to the front of each outlet to cut the number of NoMatch exceptions per navigation.
Which @angular/* package(s) are the source of the bug?
router
Is this a regression?
No
Description
In production, Safari 17.6 users sporadically click a link and the navigation neither completes nor fails: no
NavigationEnd, noNavigationError, and the promise returned byrouter.navigate()never settles. Instead, the app'sErrorHandlerreceives the router's internalNoMatch(empty message,segmentGroupproperty) throughNgZone.onError, reported by zone.js as an unhandled rejection.The app uses zone.js 0.15.1 with
provideZoneChangeDetection()and is built with@angular/build, so async/await is lowered to generators with esbuild's__asynchelper. Mapped against the production chunks, the stack reads:[email protected]:1:38159NoMatchconstructor@chunk-FFUPTH3R.js:1:45633throw new NoMatch(rawSegment)inRecognizer.matchSegmentAgainstRoutenext@[native code][email protected]:1:1425__asyncstep:o = f => { try { i(c.next(f)) } catch (j) { e(j) } }@polyfills.js:2:562thencallback, whosecatchrejects the chained promiseOur reading:
matchSegmentAgainstRouteresumes afterawait firstValueFrom(matchWithChecks(…))and throwsNoMatch, as designed.try/catchinodoes not catch it, although it surrounds thec.next(f)call that threw. JavaScript semantics rule this out, so we suspect a defect in Safari 17.6's JavaScriptCore.thenwrapper, rejects the promise fromPromise.resolve(value).then(o, p), which nobody observes, and reports it as an unhandled rejection. zone.js 0.15 passes the original value on.matchSegmentAgainstRoutenever settles, soprocessSegmentwaits forever and the navigation hangs until the next navigation supersedes it.It shows in the router because the recognizer signals "this route does not match" by throwing
NoMatchand catching it inprocessSegment. With many sibling routes in named outlets (126 children in our case), one navigation throws up to about 70 exceptions through async functions, by far the most in the application. The failure mode depends on the recognize stage using async/await (#62994). #70900 mentionsNoMatchandAbsoluteRedirectescaping in production; this may be the same thing.Suggestion: signal "no match" with a return value instead of an exception. For example,
processSegmentAgainstRouteandmatchSegmentAgainstRoutecould resolve to a sentinel thatprocessSegmentchecks before it tries the next route. That takes the exception flow through async/await out of every navigation, whatever the engine does, and saves creating dozens ofErrorobjects with stack traces per navigation.Please provide a link to a minimal reproduction of the bug
Not reproduced outside Safari 17.6 yet, see "Anything else?".
Please provide the exception or error you saw
Please provide the environment you discovered this bug in (run
ng version)Anything else?
Reproduction attempts: the production chunks, run against our route configuration in JavaScriptCore via Bun 1.0.0 to 1.3.5 (20,000 to 40,000 recognitions each, plain and inside NgZone, with aggressive JIT settings), never failed. In the field the failures are sporadic and sit between successful navigations to the same URLs.
Workaround in our app, in case it helps others: the
ErrorHandlerrecognizes the escapedNoMatch('segmentGroup' in error) and restartsrouter.currentNavigation()while itsfinalUrlis unset, in asetTimeoutbecause zone.js reports unhandled rejections after draining the microtask queue. We also moved the most used routes to the front of each outlet to cut the number ofNoMatchexceptions per navigation.