---
title: Navigation Guard Infinite Redirect Loops
impact: HIGH
impactDescription: Misconfigured navigation guards can trap users in infinite redirect loops, crashing the browser or making the app unusable
type: gotcha
tags: [vue3, vue-router, navigation-guards, redirect, debugging]
---

# Navigation Guard Infinite Redirect Loops

**Impact: HIGH** - A common mistake in navigation guards is creating conditions that cause infinite redirects. Vue Router will detect this and show a warning, but in production, it can crash the browser or create a broken user experience.

## Task Checklist

- [ ] Always check if already on target route before redirecting
- [ ] Test guard logic with all possible navigation scenarios
- [ ] Add route meta to control which routes need protection
- [ ] Use Vue Router devtools to debug redirect chains

## The Problem

```javascript
// WRONG: Infinite loop - always redirects to login, even when on login!
router.beforeEach((to, from) => {
  if (!isAuthenticated()) {
    return '/login'; // Redirects to /login, which triggers guard again...
  }
});

// WRONG: Circular redirect between two routes
router.beforeEach((to, from) => {
  if (to.path === '/dashboard' && !hasProfile()) {
    return '/profile';
  }
  if (to.path === '/profile' && !isVerified()) {
    return '/dashboard'; // Back to dashboard, which goes to profile...
  }
});
```

**Error you'll see:**

```
[Vue Router warn]: Detected an infinite redirection in a navigation guard when going from "/" to "/login". Aborting to avoid a Stack Overflow.
```

## Solution 1: Exclude Target Route

```javascript
// CORRECT: Don't redirect if already going to login
router.beforeEach((to, from) => {
  if (!isAuthenticated() && to.path !== '/login') {
    return '/login';
  }
});

// CORRECT: Use route name for cleaner check
router.beforeEach((to, from) => {
  const publicPages = ['Login', 'Register', 'ForgotPassword'];

  if (!isAuthenticated() && !publicPages.includes(to.name)) {
    return { name: 'Login' };
  }
});
```

## Solution 2: Use Route Meta Fields

```javascript
// router.js
const routes = [
  {
    path: '/login',
    name: 'Login',
    component: Login,
    meta: { requiresAuth: false },
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: Dashboard,
    meta: { requiresAuth: true },
  },
  {
    path: '/public',
    name: 'PublicPage',
    component: PublicPage,
    meta: { requiresAuth: false },
  },
];

// Guard checks meta field
router.beforeEach((to, from) => {
  // Only redirect if route requires auth
  if (to.meta.requiresAuth && !isAuthenticated()) {
    return { name: 'Login', query: { redirect: to.fullPath } };
  }
});
```

## Solution 3: Handle Redirect Chains Carefully

```javascript
// CORRECT: Break potential circular redirects
router.beforeEach((to, from) => {
  // Prevent redirect loops by tracking redirect depth
  const redirectCount = to.query._redirectCount || 0;

  if (redirectCount > 3) {
    console.error('Too many redirects, stopping at:', to.path);
    return '/error'; // Escape hatch
  }

  if (needsRedirect(to)) {
    return {
      path: getRedirectTarget(to),
      query: { ...to.query, _redirectCount: redirectCount + 1 },
    };
  }
});
```

## Solution 4: Centralized Redirect Logic

```javascript
// guards/auth.js
export function createAuthGuard(router) {
  const publicRoutes = new Set(['Login', 'Register', 'ForgotPassword', 'ResetPassword']);
  const guestOnlyRoutes = new Set(['Login', 'Register']);

  router.beforeEach((to, from) => {
    const isPublic = publicRoutes.has(to.name);
    const isGuestOnly = guestOnlyRoutes.has(to.name);
    const isLoggedIn = isAuthenticated();

    // Not logged in, trying to access protected route
    if (!isLoggedIn && !isPublic) {
      return { name: 'Login', query: { redirect: to.fullPath } };
    }

    // Logged in, trying to access guest-only route (like login page)
    if (isLoggedIn && isGuestOnly) {
      return { name: 'Dashboard' };
    }

    // All other cases: proceed
  });
}
```

## Debugging Redirect Loops

```javascript
// Add logging to understand the redirect chain
router.beforeEach((to, from) => {
  console.log(`Navigation: ${from.path} -> ${to.path}`);
  console.log('Auth state:', isAuthenticated());
  console.log('Route meta:', to.meta);

  // Your guard logic here
});

// Or use afterEach for confirmed navigations
router.afterEach((to, from) => {
  console.log(`Navigated: ${from.path} -> ${to.path}`);
});
```

## Common Redirect Loop Patterns

| Pattern                       | Problem                               | Fix                                              |
| ----------------------------- | ------------------------------------- | ------------------------------------------------ |
| Auth check without exclusion  | Login redirects to login              | Exclude `/login` from check                      |
| Role-based with circular deps | Admin -> User -> Admin                | Use single source of truth for role requirements |
| Onboarding flow               | Step 1 -> Step 2 -> Step 1            | Track completion state properly                  |
| Redirect query handling       | Reading redirect creates new redirect | Process redirect only once                       |

## Key Points

1. **Always exclude the target route** - Never redirect to a route that would trigger the same redirect
2. **Use route meta fields** - Cleaner than path string comparisons
3. **Test edge cases** - Direct URL access, refresh, back button
4. **Add logging during development** - Helps trace redirect chains
5. **Have an escape hatch** - Error page or max redirect count

## Reference

- [Vue Router Navigation Guards](https://router.vuejs.org/guide/advanced/navigation-guards.html)
- [Vue Router Route Meta Fields](https://router.vuejs.org/guide/advanced/meta.html)
