Repository navigation
Description
Which @angular/* package(s) are the source of the bug?
router
Is this a regression?
No
Description
When a navigation is cancelled or fails after its URL has already been committed, NavigationStateManager.cancel() puts the previous URL back with navigation.navigate(previousUrl, {history: 'replace', info: {ɵrouterInfo: {intercept: false}}}). handleNavigate then returns early for that event without intercepting it. A navigation that nobody intercepts is a regular one, so the browser loads the previous URL as a new document and the whole app restarts.
I could reproduce it in these cases, all with the default canceledNavigationResolution: 'replace':
- Chrome, default config: a routed component throws in its constructor (an
inject()without a provider, for example). In'deferred'mode the URL is committed right before activation, so the error goes through the rollback. - Chrome with
urlUpdateStrategy: 'eager': any guard that rejects. - Safari 26.6, which doesn't support
precommitHandler, so the URL is committed as soon as the event is intercepted: a guard that rejects asynchronously after a plain link click, the back button on a page whosecanDeactivateasks for confirmation and gets "no", and stopping a navigation in progress (window.stop()).
The canDeactivate case is the worst one: the guard is there to keep unsaved changes, and the reload throws them away.
With the History API integration, the same rollback is a replaceState() and the page stays. The router's tests expect that for both integrations ("should dispatch NavigationError after the url has been reset back" and "can renavigate to throwing component" run with the Navigation API too). They pass there only because FakeNavigation never loads a new document.
With canceledNavigationResolution: 'computed', rollbacks after a push navigation use traverseTo() instead, which stays in the same document, so most of these cases don't reload there.
#71597 intercepts the rollback navigation, without a handler, so only the URL changes.
Please provide a link to a minimal reproduction of the bug
No hosted reproduction, but this is self-contained:
import {Component} from '@angular/core';
import {bootstrapApplication} from '@angular/platform-browser';
import {
RouterLink,
RouterOutlet,
provideRouter,
withExperimentalPlatformNavigation,
} from '@angular/router';
@Component({template: ''})
class Home {}
@Component({template: ''})
class Broken {
constructor() {
throw new Error('Broken');
}
}
@Component({
selector: 'app-root',
imports: [RouterLink, RouterOutlet],
template: `
<p>Loaded at {{ loadedAt }}</p>
<a routerLink="/broken">Go to a broken page</a>
<router-outlet />
`,
})
class App {
loadedAt = new Date().toLocaleTimeString();
}
bootstrapApplication(App, {
providers: [
provideRouter(
[
{path: '', component: Home},
{path: 'broken', component: Broken},
],
withExperimentalPlatformNavigation(),
),
],
});In Chrome, click the link. The error is logged, then the page reloads: "Loaded at" changes and the server gets a new request for /. Without withExperimentalPlatformNavigation(), the URL goes back to / and the page stays as it is.
Please provide the exception or error you saw
No error from the rollback itself, the page just reloads.
Please provide the environment you discovered this bug in (run ng version)
Angular 22.2.1, Chrome 155 and Safari 26.6.2. Current main has the same code.
Anything else?
One related thing in Safari that the fix doesn't cover: when a guard rejects synchronously after a plain link click or the back button, cancel() runs while the navigate event is still being dispatched. At that point the URL hasn't changed yet, so there's nothing to roll back, but the browser commits the intercepted navigation right after. The URL ends up on the rejected route while the router stays on the previous page. That's a different code path, so I left it out.
Activity
fix(router): don't reload the page when restoring the URL with the Na…