Skip to content

Router: NoMatch escapes route recognition on Safari 17.6 and the navigation never settles #71038

Description

@MilesAl

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:

  1. The generator of matchSegmentAgainstRoute resumes after await firstValueFrom(matchWithChecks(…)) and throws NoMatch, as designed.
  2. 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.
  3. 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.
  4. 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.

Activity

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

    area: routergemini-triagedLabel noting that an issue has been triaged by gemini

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions