In Angular, what can a route guard do by returning a RedirectCommand that returning a plain UrlTree cannot?
answer
- a UrlTree plus navigation options
- what the redirect inherits by default
- skipLocationChange, replaceUrl, state, browserUrl
- since 22.2 it can be thrown
basics
~20 sA RedirectCommand (v18+) wraps the target UrlTree with NavigationBehaviorOptions that override what the redirect would otherwise inherit from the original navigation, so a guard can set skipLocationChange, replaceUrl, state, info or browserUrl for the redirect.
solid answer
~40 sBoth results make the router cancel the current navigation and start a redirect. With a bare `UrlTree`, the redirect reuses the original navigation's extras: `skipLocationChange`, `info`, `browserUrl`, `scroll`, and `replaceUrl` when the original used it, when URL updates are `'eager'`, or when the browser triggered it. It does not carry `state`. `new RedirectCommand(urlTree, navigationBehaviorOptions)` spreads its own options over those defaults. That lets an admin guard render `/forbidden` while keeping the requested URL in the address bar with `browserUrl: state.url`, or send users to `/login` with `skipLocationChange: true` or `replaceUrl: true` so Back does not return to the blocked page. `RedirectCommand` arrived in v18 for guards and resolvers, and since 22.2 a guard may also `throw` one, because the class extends `Error`.
code
ts · 19 linesimport { inject } from '@angular/core';
import { CanActivateFn, RedirectCommand, Router } from '@angular/router';
import { AuthService } from './auth.service';
export const adminOnly: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (!auth.isSignedIn()) {
return new RedirectCommand(router.parseUrl('/login'), { replaceUrl: true });
}
if (!auth.hasRole('admin')) {
return new RedirectCommand(router.parseUrl('/forbidden'), {
browserUrl: state.url,
state: { reason: 'role-required' },
});
}
return true;
};go deeper
Recall that a guard can redirect by returning either a UrlTree or a RedirectCommand, and that RedirectCommand takes navigation options.
Explain which extras a UrlTree redirect inherits from the cancelled navigation and how RedirectCommand options override them.
Use replaceUrl and browserUrl deliberately so sign-in and forbidden redirects leave clean history and reload-safe URLs, and know the 22.2 throw form.
Standardise redirect behaviour across guards so history, reload and deep-link semantics are consistent for every protected area.
## Two ways to redirect from a guard An Angular route guard returns a `GuardResult`, which is `boolean | UrlTree | RedirectCommand` (optionally inside an `Observable` or `Promise`). Both `UrlTree` and `RedirectCommand` mean **redirect**: the router cancels the current navigation with a redirect cancellation code and schedules a new navigation to the target. The difference is how that new navigation behaves. - A **`UrlTree`** is only a destination, typically built with `router.createUrlTree([...])` or `router.parseUrl('...')`. - A **`RedirectCommand`** is a destination plus options: `new RedirectCommand(redirectTo: UrlTree, navigationBehaviorOptions?: NavigationBehaviorOptions)`. ## What a plain UrlTree redirect inherits When the router handles a redirect, it builds the extras for the new navigation from the one being cancelled: | Extra | Value for a `UrlTree` redirect | |---|---| | `skipLocationChange` | Copied from the original navigation | | `info` | Copied from the original navigation | | `browserUrl` | Copied from the original navigation | | `scroll` | Copied from the original navigation | | `replaceUrl` | `true` if the original used it, if `urlUpdateStrategy` is `'eager'`, or if the browser triggered the navigation (Back, Forward, address bar) | | `state` | Not carried over | The `replaceUrl` inheritance was a v18 behaviour change: before it, a redirect could push a new history entry even when the original navigation asked to replace one. A `RedirectCommand`'s options are spread **over** these defaults, so anything it sets wins. ## What RedirectCommand adds `NavigationBehaviorOptions` includes, among others: - `skipLocationChange`: navigate without updating the browser URL. - `replaceUrl`: replace the current history entry instead of pushing one. - `state`: an object stored in `history.state` for the new entry. - `info`: arbitrary data attached to the navigation, readable through the router's current navigation but not stored in history. - `browserUrl`: show a different URL in the address bar than the one the router matched. - `onSameUrlNavigation` and `scroll`: per-navigation overrides of router configuration. ## Admin-area examples 1. **Forbidden page without losing the URL.** A signed-in non-admin opens `/admin/users`. The guard returns `new RedirectCommand(router.parseUrl('/forbidden'), { browserUrl: state.url })`. The forbidden component renders while the address bar still reads `/admin/users`, so a reload after gaining the role goes to the right place. `browserUrl` affects only the address bar: the router state, params and data belong to `/forbidden`. 2. **Sign-in without a dead history entry.** A signed-out user follows a link to `/admin`. Returning `new RedirectCommand(router.parseUrl('/login'), { replaceUrl: true })` avoids leaving an entry that Back would return to, only to be redirected again. 3. **Passing context.** `state: { reason: 'role-required' }` lets the forbidden page explain why the user landed there without encoding it in the URL. ## Throwing a RedirectCommand (22.2) `RedirectCommand` extends `Error`. Since Angular 22.2 the router treats a thrown `RedirectCommand` from a guard or resolver the same as a returned one: its navigation error handling converts it into a redirect rather than a `NavigationError`. This helps when the decision is made deep inside a helper that would otherwise have to thread a return value back up. Returning it remains the clearer default. ## Pitfalls - **Expecting `browserUrl` to change route state.** The forbidden component reads the params and data of `/forbidden`, not of the URL in the address bar; code that reads `location.path()` sees a different URL from the router's. - **Hiding a redirect from history with `skipLocationChange`.** The address bar keeps the previous URL, so a reload returns to the page the user was redirected away from and the guard runs again. That is sometimes the goal and sometimes a loop. - **Putting persistent data in `info`.** `info` is attached to the current `Navigation` object only; it is not written to `history.state`, so it is gone after a reload. Use `state` for what the next page must still see on Back. - **Relying on the thrown form on older versions.** Before 22.2 a thrown `RedirectCommand` is just an error and ends the navigation with a `NavigationError`. ## When a UrlTree is enough - The redirect should behave like the navigation it replaces, which is usually right for simple sign-in redirects. - The codebase must stay readable to developers who expect `createUrlTree` in guards. Reach for `RedirectCommand` when the redirect's history or address-bar behaviour must differ from the original navigation.
- In Angular, does browserUrl on a RedirectCommand change the params and data that the target route's component reads?No. `browserUrl` only changes what the address bar shows. The router state, including `params`, `queryParams` and `data`, comes from the URL actually matched, here `/forbidden`. Components must not read `location` expecting it to match the router state when `browserUrl` or `skipLocationChange` is in use.
saying these in an interview costs you the question
- A UrlTree redirect always pushes a fresh history entry regardless of the original navigation.
- RedirectCommand is only accepted from resolvers, not from guards.
- browserUrl changes the params the redirected component receives.
- A guard that throws anything, including a RedirectCommand, always produces a NavigationError.
- A UrlTree redirect carries over the state object from the original navigate() call.