skip to content

In Vue Router 5, a job-search page runs `await router.push()` for each filter change; what can that promise settle with, and how do you tell the outcomes apart?

level: seniorimportance: should knowfreq 45%

answer

  1. three outcomes, not two
  2. failures resolve, errors reject
  3. aborted, cancelled, duplicated
  4. isNavigationFailure with a type
  5. redirectedFrom marks a redirect

basics

~20 s

router.push() resolves undefined on success, including after a guard redirect; resolves with a navigation failure (aborted, cancelled or duplicated) when the user stays put; and rejects when a guard or lazy component throws. isNavigationFailure(result, type) tells failures apart.

solid answer

~40 s

In Vue Router 5 the promise has three outcomes. Success resolves `undefined`, and so does a guard redirect, which starts a new navigation; `router.currentRoute.value.redirectedFrom` shows it happened. When the user stays on the page for a known reason it resolves with a navigation failure, an `Error` with `type`, `to` and `from`: `duplicated` when the target equals the current location including query and hash, `aborted` when a guard returned `false`, `cancelled` when a newer navigation started first. I check with `isNavigationFailure(failure, NavigationFailureType.aborted)`. When a guard throws or a lazy chunk fails, `router.onError()` handlers run and the promise rejects. So I close the filter drawer only when the result is falsy, and I report failures centrally in `afterEach`, whose third argument is the failure.

code

ts · 20 lines
ts
import {
  createRouter,
  createWebHistory,
  isNavigationFailure,
  NavigationFailureType,
} from 'vue-router'
import { routes } from './routes'
import { reportNavigation } from './telemetry'

export const router = createRouter({ history: createWebHistory(), routes })

router.afterEach((to, from, failure) => {
  if (isNavigationFailure(failure, NavigationFailureType.aborted)) {
    reportNavigation('blocked', from.fullPath, to.fullPath)
  }
})

router.onError((error, to) => {
  reportNavigation('error', to.fullPath, String(error))
})

go deeper

for a junior

Recall that router.push returns a Promise and that awaiting it tells you whether the navigation actually happened.

for a middle

Explain the three failure types, duplicated, aborted and cancelled, and why they resolve while thrown errors reject.

for a senior

Show defensive navigation code: act only on a falsy result, split failures with isNavigationFailure, report through afterEach and onError, and recognise redirects via redirectedFrom.

for a principal

Decide how navigation outcomes are observed app-wide, which failures are noise and which are signals, so dashboards do not alarm on routine cancellations.

## What `router.push()` returns In Vue Router 5, `router.push()`, `router.replace()` and RouterLink's internal `navigate` all return a **Promise**. Navigation is asynchronous, because guards may be async and lazy route components may need downloading, so the promise is how code learns what happened. It settles in one of three ways: | Outcome | Promise | Value | |---|---|---| | navigation completed, including through a guard redirect | resolves | `undefined` | | user stays on the current page for a known reason | resolves | a **navigation failure** | | a guard or a lazy component threw an unexpected error | **rejects** | the thrown error | ## Navigation failures and their types A navigation failure is an `Error` instance with three extra properties: `type`, `to` and `from`, the last two being normalized route locations. `NavigationFailureType` names the three types: - **`duplicated`**: the target is already the current location, with the same route record, params, query and hash. Clicking *Apply* on a job-search page with unchanged filters produces it. No `beforeEach` or component guard runs and no history entry is added; only the `afterEach` hooks see it. - **`aborted`**: a navigation guard returned `false` (or called the deprecated `next(false)`). The URL stays on `from`. - **`cancelled`**: a **newer** navigation started before this one finished. A user who clicks two filters quickly while an async guard is still pending cancels the first push; the second proceeds. Test for them with `isNavigationFailure(value, type)`. Without the second argument it returns `true` for any failure. The types are bit flags, so `NavigationFailureType.aborted | NavigationFailureType.cancelled` checks for either. ```ts import { isNavigationFailure, NavigationFailureType } from 'vue-router' const failure = await router.push({ query: nextFilters }) if (!failure) closeFilterDrawer() else if (isNavigationFailure(failure, NavigationFailureType.aborted)) showNotice('Filters not applied') ``` ## Redirects are successes A guard that returns a route location does not fail the navigation; it **starts a new one**, and the original promise resolves with that new navigation's result, `undefined` when it completes. To know a redirect happened, read `router.currentRoute.value.redirectedFrom` after awaiting. Code that treats a redirect as a failure misreads a normal login redirect. ## Thrown errors reject If a guard throws, or a lazy route component fails to load, the router: 1. calls every handler registered with `router.onError()`, or logs the error to the console, with a development warning, when none is registered; 2. **rejects** the promise with the error; 3. skips the `afterEach` hooks for that navigation. An `await router.push()` with no `try/catch` therefore turns a failed chunk download into an unhandled rejection. RouterLink avoids this by catching the rejection internally, and the error still reaches `onError`. ## Global reporting `router.afterEach((to, from, failure) => …)` receives the failure as its third argument, for completed and failed navigations alike. That makes it the one place to count duplicated, aborted and cancelled navigations across the app. Rejections never reach it; register `router.onError()` for those. ## Forcing a navigation `force: true` in the location skips the duplicate check, so a *Refresh results* button can send the same URL through the guards again. It also adds a history entry unless `replace: true` is set as well. ## The job-search page, action by action | User action | Outcome of the push | |---|---| | picks a new salary range | resolves `undefined`; URL and results update | | clicks *Apply* with nothing changed | resolves with a `duplicated` failure | | picks a premium-only filter that a guard blocks by returning `false` | resolves with an `aborted` failure | | picks two filters in quick succession | the first resolves `cancelled`, the second `undefined` | | opens a saved search whose guard redirects to sign-in | resolves `undefined`; `redirectedFrom` is set | | opens a posting whose lazy chunk fails to download | rejects; `onError` handlers run | Reading the table from the code's side: only the first and fifth rows are successes with a falsy result, the second needs nothing at all, the third deserves a message, the fourth is routine, and the last is an incident to report. ## Common mistakes - Closing the filter drawer after `await router.push()` without checking the result. - Wrapping every push in `try/catch` to catch duplicates: duplicates resolve, they never reject. - Surfacing `cancelled` to the user as an error; it usually means the user already moved on. - Treating a falsy result as blocked and a failure object as success, which inverts the protocol.

  • How do you report every failed navigation across the app in one place?
    Register `router.afterEach((to, from, failure) => …)` and report when `failure` is set, using `isNavigationFailure` to split duplicated, aborted and cancelled. Add `router.onError()` for thrown errors and failed lazy chunks, because those reject the push and never reach `afterEach`.
  • When would you pass `force: true` to `router.push`?
    When the same URL must go through the navigation pipeline again, such as a Refresh button whose data loading runs in a navigation guard. Without it the router resolves with a `duplicated` failure and does nothing. `force` also adds a history entry, so pair it with `replace: true` if Back should not land on the same page twice.
  • What do a failure's `to` and `from` hold for an aborted push from `/jobs` to `/jobs/42`?
    Both are normalized route locations: `to.fullPath` is `/jobs/42`, the target that was blocked, and `from.fullPath` is `/jobs`, where the user still is. They let a handler say which move was refused without keeping its own copy of the target.

A push is like sending a parcel with tracking: it is delivered (undefined), returned to sender with a stamped reason such as address unchanged or refused (a failure you can read), or lost in a crash (a rejection someone must handle).

saying these in an interview costs you the question

  • router.push rejects when you navigate to the page you are already on
  • A guard redirect makes router.push resolve with a navigation failure
  • An undefined push result means the navigation was blocked
  • afterEach receives errors thrown inside navigation guards
  • A cancelled failure means a guard returned false