In Angular's Router, what is the difference between navigate() and navigateByUrl(), and which one honours relativeTo and queryParams?
answer
- commands versus a finished URL
- createUrlTree runs first
- default starting point is the root
- Promise of boolean
basics
~10 sRouter.navigate() builds a UrlTree from a commands array, honouring relativeTo, queryParams, queryParamsHandling and fragment, then calls navigateByUrl(). navigateByUrl() takes a complete URL string or UrlTree and applies no delta. Both return a Promise<boolean>.
solid answer
~40 s`navigate(commands, extras)` is `createUrlTree(commands, extras)` followed by `navigateByUrl(tree, extras)`. Only the URL-building step reads `relativeTo`, `queryParams`, `queryParamsHandling`, `fragment` and `preserveFragment`, so those options work with `navigate()` and are not part of `navigateByUrl()`'s options at all. `navigateByUrl()` takes an absolute string, parsed including its query string and fragment, or a prebuilt `UrlTree`, plus behaviour options: `replaceUrl`, `skipLocationChange`, `state`, `info`, `browserUrl`, `onSameUrlNavigation`. One trap: `navigate()` without `relativeTo` resolves from the **root**, unlike `routerLink`, which defaults to its own route. Both return `Promise<boolean>`: `true` on success, `false` when a guard blocks, a newer navigation supersedes it or a same-URL navigation is ignored; errors reject it unless `resolveNavigationPromiseOnError` is set.
code
ts · 23 linesimport {Component, inject, input} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
@Component({
selector: 'app-product-detail',
template: `<button (click)="edit()">Edit</button> <button (click)="back()">Back</button>`,
})
export class ProductDetail {
private readonly router = inject(Router);
private readonly route = inject(ActivatedRoute);
readonly returnUrl = input<string>('/products');
edit(): void {
// /products/42 -> /products/42/edit
this.router.navigate(['edit'], {relativeTo: this.route});
}
async back(): Promise<void> {
const tree = this.router.parseUrl(this.returnUrl());
const ok = await this.router.navigateByUrl(tree, {replaceUrl: true});
if (!ok) console.warn('Navigation was blocked or superseded');
}
}go deeper
Remember that navigate takes an array of commands and navigateByUrl takes a whole URL string or UrlTree, and that both are called on the injected Router.
Explain that navigate is createUrlTree plus navigateByUrl, which options belong to URL creation versus navigation behaviour, and that navigate defaults to the root route.
Use the boolean result to react to blocked or superseded navigations, and pass UrlTrees around instead of concatenated strings so return URLs stay correctly encoded.
Standardise how a codebase expresses destinations, typed helpers returning UrlTrees for example, so relative-versus-absolute mistakes stop recurring across teams.
## Two entry points, one pipeline Angular's `Router` service (from `@angular/router`, obtained with `inject(Router)`) has two imperative navigation methods. They are not independent: `navigate()` is a thin wrapper. ```ts // What Router.navigate does, simplified navigate(commands, extras) { return this.navigateByUrl(this.createUrlTree(commands, extras), extras); } ``` So the real question is which options belong to **URL creation** and which to **navigation behaviour**. ## The two option types The router splits its options into two interfaces, and `NavigationExtras` is simply both together: | Interface | Fields | Read by | |---|---|---| | `UrlCreationOptions` | `relativeTo`, `queryParams`, `queryParamsHandling`, `fragment`, `preserveFragment` | `createUrlTree()`, therefore `navigate()` and `routerLink` | | `NavigationBehaviorOptions` | `replaceUrl`, `skipLocationChange`, `state`, `info`, `browserUrl`, `onSameUrlNavigation` | `navigateByUrl()` | | `NavigationExtras` | both of the above | `navigate()` | Consequences: - `navigate(['..', 'list'], {relativeTo: this.route, queryParams: {page: 2}})` works: the commands are applied relative to that route and the query is attached. - `navigateByUrl('/products', {queryParams: {page: 2}})` does not compile, because `NavigationBehaviorOptions` has no `queryParams`. If the object reaches it through a wider type, the field is simply never read. - `navigateByUrl('/products?page=2#top')` does carry a query and fragment, because the **string is parsed** by the `UrlSerializer`. ## Relative or absolute `navigateByUrl()` applies no delta: whatever string you pass is parsed as a whole URL, so it behaves as absolute navigation. `navigate()` can be relative, but only when told: 1. With `relativeTo: this.route`, commands without a leading `/` apply under that route, and `../` climbs. 2. Without `relativeTo`, or with `relativeTo: null`, the commands are applied from the **root**. 3. A leading `/` in the first command makes it absolute regardless of `relativeTo`. This is the classic surprise: `router.navigate(['edit'])` from a component at `/products/42` goes to `/edit`, while `<a routerLink="edit">` in the same component points at `/products/42/edit`, because the directive defaults `relativeTo` to its own `ActivatedRoute`. ## `UrlTree` as the common currency A `UrlTree` is the router's parsed URL: a tree of segment groups (one per outlet) plus `queryParams` and `fragment`. You obtain one from `router.createUrlTree(commands, options)` or `router.parseUrl(string)`, and turn it back into text with `router.serializeUrl(tree)`. `navigateByUrl()` accepts a tree directly, and a clicked `routerLink` itself calls `navigateByUrl()` with the tree it built. Building the tree once lets you log it, compare it, show it as an `href` or pass it on without string concatenation. ## What the returned Promise means Both methods return `Promise<boolean>`: - `true` - the navigation completed; - `false` - it did not: a guard returned `false`, a newer navigation superseded it, or it targeted the current URL while `onSameUrlNavigation` is `'ignore'`, the default; - rejected - an error was thrown during navigation, unless `withRouterConfig({resolveNavigationPromiseOnError: true})` turns that into `false` as well. Ignoring the result is fine for fire-and-forget links; code that must react to a blocked navigation, such as closing a dialog only after leaving, should check it. ## A worked comparison From a component routed at `products/:id`, currently showing `/products/42?tab=specs`: - `router.navigate(['reviews'], {relativeTo: this.route})` goes to `/products/42/reviews`; the old `tab` query is dropped because the default query handling replaces it. - `router.navigate(['reviews'])` goes to `/reviews`, because no `relativeTo` means the root. - `router.navigateByUrl('reviews')` also lands on `/reviews`: the string is parsed as a whole URL, never applied as a delta. - `router.navigateByUrl(router.createUrlTree(['reviews'], {relativeTo: this.route}))` is exactly what the first call does internally. The last line is a useful way to explain `navigate()` in an interview: it is URL building plus `navigateByUrl()`, and every surprise comes from the building step's defaults. ## Choosing between them - Use **`navigate()`** when you have pieces: IDs, a route to be relative to, query params to merge. - Use **`navigateByUrl()`** when you already hold a complete URL, such as a stored return URL or a `UrlTree` you built, or when you only need behaviour options. - Prefer a template `routerLink` over either when the user is clicking a visible link, so the page gets a real `href`.
- Why does router.navigate(['edit']) in a component at /products/42 land on /edit?Without relativeTo, navigate() builds the URL tree from the root route, so a relative command is applied at the top. routerLink differs because it defaults relativeTo to the ActivatedRoute where the directive is used. Pass relativeTo: this.route to get /products/42/edit.
- When would you build a UrlTree with createUrlTree() instead of navigating immediately?When the URL is a value you need before or instead of navigating: rendering it as an href, logging or comparing it, storing it as a return URL, or handing it to navigateByUrl() later. createUrlTree() uses the same command rules as navigate() without triggering a navigation.
- What does navigate() return when a navigation to the current URL is ignored?It resolves to false. With the default onSameUrlNavigation of 'ignore', the router emits NavigationSkipped and does nothing else; set onSameUrlNavigation to 'reload' to rerun guards and resolvers for the same URL.
saying these in an interview costs you the question
- Passing queryParams to navigateByUrl and expecting them in the URL
- Assuming Router.navigate defaults relativeTo to the calling component's route
- Believing navigateByUrl cannot carry a query string or fragment
- Treating the returned Promise as always resolving true once called
- Thinking navigate and navigateByUrl run different navigation pipelines