skip to content

In Angular's Router, what is the difference between navigate() and navigateByUrl(), and which one honours relativeTo and queryParams?

level: middleimportance: must knowfreq 62%

answer

  1. commands versus a finished URL
  2. createUrlTree runs first
  3. default starting point is the root
  4. Promise of boolean

basics

~10 s

Router.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 lines
ts
import {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

for a junior

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.

for a middle

Explain that navigate is createUrlTree plus navigateByUrl, which options belong to URL creation versus navigation behaviour, and that navigate defaults to the root route.

for a senior

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.

for a principal

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