skip to content

In Vue Router 5, what happens to a navigation when a `beforeEach` guard returns nothing, returns a route location, returns `false`, or throws?

level: middleimportance: must knowfreq 66%

answer

  1. the return value is the verdict
  2. nothing or true: continue
  3. false: cancel, URL back to from
  4. location: drop and restart
  5. throw: cancel and onError

basics

~20 s

Nothing or true lets the navigation continue to the next guard; a route location drops it and starts a new navigation there; false cancels it and resets the URL to from; a thrown Error cancels it and calls router.onError() handlers.

solid answer

~40 s

In Vue Router 5 a guard declared with `to` and `from` only is resolved from its return value, and async guards work the same through their promise. Returning nothing, `undefined` or `true` validates the navigation and the next guard runs. Returning a route location, a string or an object with the same options `router.push()` takes such as `replace: true`, drops the current navigation and starts a new one from the same `from`; the result is recorded in `redirectedFrom`. Returning `false` cancels it, and if the URL had already changed, through Back or Forward, the router resets it to `from`. Throwing an `Error` also cancels, and the router calls every `router.onError()` handler with the error, `to` and `from`, or logs it to the console when none is registered; the push promise rejects.

code

ts · 16 lines
ts
router.beforeEach(async (to) => {
  if (!to.meta.requiresAuth) return
  const auth = useAuthStore()
  if (!auth.user) {
    return { name: 'login', query: { redirect: to.fullPath } }
  }
  const roles = await auth.loadRoles()
  const needed = to.meta.roles
  if (needed && !needed.some((r) => roles.includes(r))) {
    return { name: 'forbidden' }
  }
})

router.onError((error, to, from) => {
  reportNavigationError(error, from.fullPath, to.fullPath)
})

go deeper

for a junior

Recall the four answers a guard can give: nothing to continue, a location to redirect, false to cancel, a thrown error to fail.

for a middle

Explain each branch's side effects: redirects restart the guard pipeline from the same from, false restores the URL, and throws reach router.onError and reject the push.

for a senior

Show judgment in real guards: redirect to an explanation instead of a silent false, never return router.push, and let unexpected failures reach onError.

for a principal

Set a team rule for what each verdict means in the product, cancel, redirect or fail, so guards across modules behave and report consistently.

## The return value is the verdict In Vue Router 5 a navigation guard answers **by what it returns**. The router wraps each guard in a promise, so a synchronous guard and an `async` one follow the same rules: an `async` guard's verdict is whatever its promise resolves to. The protocol: | Guard returns | Effect on the navigation | What the caller of `router.push()` sees | |---|---|---| | nothing, `undefined` or `true` | validated; the next guard runs | resolves `undefined` if all guards pass | | a route location | current navigation dropped; a new one starts | resolves with the new navigation's result | | `false` | cancelled; URL reset to `from` | resolves with an aborted navigation failure | | throws an `Error` | cancelled; `router.onError()` handlers called | the promise rejects | This applies to every before guard: `beforeEach`, `beforeResolve`, a record's `beforeEnter` and the in-component guards. ## Sync and async guards behave the same The router does not care whether a guard is `async`. For a guard declared with only `to` and `from`, it: 1. calls the guard and wraps whatever comes back in a promise; 2. waits for that promise, so an `await` inside the guard simply delays the navigation, which stays **pending** meanwhile; 3. applies the protocol above to the resolved value, or treats a rejected promise as a throw. A slow guard therefore delays every navigation it applies to; the user keeps seeing the current page until it answers. ## Nothing or `true`: continue The everyday branch. An HR-portal guard that has nothing to object to simply falls off the end of the function. There is no need to write `return true`, and no need to call anything. ## A route location: redirect Returning a location **redirects**. It can be a string such as `'/login'` or an object such as `{ name: 'login', query: { redirect: to.fullPath } }`, and it accepts the same options as `router.push()`, including `replace: true`. The router drops the pending navigation and starts a new one with the **same `from`**; that new navigation runs every guard again. When it completes, `router.currentRoute.value.redirectedFrom` points at the location originally requested. Because the new navigation passes through the same guards, a guard must never redirect to a location it would redirect again. ## `false`: cancel and restore the URL Returning `false` cancels the navigation and the user stays where they were. If the browser URL had already changed, because the user pressed Back or Forward, the router moves the history back so the address bar shows `from` again. It gives no feedback on its own: an HR employee who clicks *Payroll* without the payroll role sees nothing happen. For access denials a redirect to a *not allowed* page is usually kinder than a silent `false`. ## Throwing: cancel and report Throwing an `Error`, or returning a promise that rejects, marks something **unexpected**, such as the role lookup failing with a network error. The router then: 1. cancels the navigation; 2. calls every handler registered with `router.onError((error, to, from) => …)`, or, if there is none, logs the error with `console.error` and a development warning suggesting one; 3. rejects the promise returned by `router.push()`. The `afterEach` hooks do not run for a navigation that ended in a thrown error. ## The HR portal guard ```ts router.beforeEach(async (to) => { if (!to.meta.requiresAuth) return const auth = useAuthStore() if (!auth.user) return { name: 'login', query: { redirect: to.fullPath } } const roles = await auth.loadRoles() const needed = to.meta.roles if (needed && !needed.some((r) => roles.includes(r))) return { name: 'forbidden' } }) router.onError((error, to) => { reportNavigationError(error, to.fullPath) }) ``` - A public page returns nothing and continues. - A signed-out user is redirected with the destination kept. - A user without the role is redirected to an explanation page. - A failing `loadRoles()` rejects the guard's promise, which the router treats like a throw. ## Common mistakes - Returning `false` for an unauthorised user and leaving them puzzled on the same page. - Returning `router.push('/login')` instead of `'/login'`: that starts a second, unrelated navigation which cancels the first one. The user reaches the login page, but the original `router.push()` resolves with a cancelled failure instead of following a redirect, and `redirectedFrom` stays empty. - Swallowing errors inside the guard with a `catch` that returns nothing, which turns a failed lookup into an allowed navigation. - Declaring a third `next` parameter and then returning values: the router then waits for `next` instead.

  • What is the difference between returning `false` and returning `{ name: 'forbidden' }` for a user without the payroll role?
    `false` cancels: the user stays on the current page, the URL is reset to `from`, and nothing explains why. A location redirects: a new navigation runs to the forbidden page, which can say what access is missing. Both stop the payroll page from rendering; the redirect also gives feedback.
  • What goes wrong if a guard returns `router.push('/login')` instead of `'/login'`?
    The push starts a second navigation, and the guard waits for it to finish. The user does reach the login page, but only because that navigation cancels the original one: the original `router.push()` resolves with a cancelled failure instead of following a redirect, and `redirectedFrom` stays empty. Returning the location itself, `'/login'`, makes the router redirect properly.

saying these in an interview costs you the question

  • Returning false sends the user back to the previous page in history
  • Guards must return true explicitly or the navigation is cancelled
  • A thrown error inside a guard is ignored and the navigation continues
  • Returning router.push('/login') from a guard is the same as returning '/login'
  • Async guards cannot redirect, only synchronous ones can