Updated June 2026. Tested on Vue 3 with Vue Router 4 and <script setup>. Part of the Techalyst Vue 3 series.

Navigation guards are hooks that run at points during a route change. They are how you protect pages behind authentication, confirm before discarding an unsaved form, set the page title, or fire analytics. Vue Router 4 has three levels of guard, and modern guards control navigation by returning a value rather than calling next(). Here is the working set.

Global guards

Registered on the router instance, global guards run for every navigation. There are three.

// router/index.js
router.beforeEach((to, from) => {
  // runs before every navigation; return to control it
})

router.beforeResolve((to, from) => {
  // runs after all component guards resolve, still able to redirect
})

router.afterEach((to, from) => {
  // runs after navigation is confirmed; cannot change it
})

beforeEach is the workhorse for auth. beforeResolve is the safe place for data fetching that should only run once entry is guaranteed. afterEach cannot affect navigation, so it is for side effects like setting document.title or logging.

Controlling navigation by returning

A guard succeeds by default, return nothing to let it through. To affect the navigation, return a value:

router.beforeEach((to) => {
  return false                 // cancel the navigation
  return '/login'              // redirect to a path
  return { name: 'login' }     // redirect to a named route
})

This return-based style replaces the old next() callback, which Vue Router 4 actively discourages. (The one place next still earns its keep is accessing a freshly created instance from beforeRouteEnter, a rare need.)

The auth pattern, and the loop trap

The canonical guard is an auth gate driven by route meta, a free-form object you attach to routes.

const routes = [
  { path: '/dashboard', name: 'dashboard', component: Dashboard, meta: { requiresAuth: true } },
  { path: '/login', name: 'login', component: Login },
]

router.beforeEach((to) => {
  const loggedIn = useAuthStore().isLoggedIn
  if (to.meta.requiresAuth && !loggedIn) {
    return { name: 'login' }
  }
})

One trap worth flagging: beforeEach runs on the redirect target too. If you redirect unconditionally you create an infinite loop, so always guard the redirect with a condition (here, to.meta.requiresAuth) so the login route itself is allowed through.

Per-route guards

When a guard only concerns one route, define beforeEnter on the route itself. It fires when entering that route, but not on param or query changes within it.

{
  path: '/admin',
  component: Admin,
  beforeEnter: (to, from) => {
    if (!isAdmin()) return { name: 'home' }
  },
}

You can also pass an array of functions to run several checks in sequence.

In-component guards

Components can guard their own route. In <script setup> the two you will reach for are composables: onBeforeRouteUpdate and onBeforeRouteLeave.

onBeforeRouteLeave is perfect for the "you have unsaved changes" confirmation, it fires while the component is still mounted, before you navigate away.

<script setup>
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'

onBeforeRouteLeave((to, from) => {
  if (hasUnsavedChanges.value) {
    const stay = !window.confirm('Discard your changes?')
    if (stay) return false // cancel the navigation
  }
})

onBeforeRouteUpdate((to, from) => {
  // same component reused with new params (e.g. /user/1 -> /user/2)
  fetchUser(to.params.id)
})
</script>

onBeforeRouteUpdate covers the case where the route changes but the same component stays mounted, which is exactly when you need to refetch data for the new params, since the component is not recreated.

Execution order

When several guards apply, they fire in a predictable order: global beforeEach, then per-route beforeEnter, then in-component enter guards, then global beforeResolve, navigation is confirmed, and finally global afterEach. The pattern is global to local for the "before" guards, with beforeResolve and afterEach always last. You rarely need to memorise the whole chain, but knowing afterEach runs last (and cannot block) explains why it is the right home for titles and analytics.

Wrapping up

Guards are your hook into the navigation lifecycle. Use global beforeEach with route meta for auth (guarding the redirect to avoid loops), beforeEnter for one-off per-route checks, and the onBeforeRouteLeave / onBeforeRouteUpdate composables for in-component confirmation and refetching. Control flow by returning false or a redirect target rather than reaching for next(), and lean on afterEach for side effects that should never block the route.

More in the series: routing a Vue SPA and dynamic routes and lazy loading. Questions welcome below.