#Uncaught ConvexError: Unauthenticated when signing out
20 messages · Page 1 of 1 (latest)
This is a known issue with the expectAuth: true setting in TanStack Start + Convex + Better Auth setups. When a user signs out, the expectAuth: true option only applies before the initial authentication — after sign-out, authenticated queries (like your useSuspenseQuery) can fire before authentication is cleared, causing the Unauthenticated error.
The recommended fix is to reload the page on sign out. [TanStack Start usage]
import { authClient } from "~/lib/auth-client";
const handleSignOut = async () => {
await authClient.signOut({
fetchOptions: {
onSuccess: () => {
location.reload();
},
},
});
};
As the docs explain:
The
expectAuth: truesetting only has effect before the initial authentication. If a user signs out and signs back in, authenticated queries will likely be called before authentication is ready, resulting in an error. For this reason, the current recommendation is to reload the page on sign out.
Also, make sure your router is configured with expectAuth: true in the ConvexQueryClient constructor, and that you reload on sign out as shown in the migration guide: [migrate to 0.10]
const convexQueryClient = new ConvexQueryClient(convexUrl, {
expectAuth: true,
});
Feel free to share the rest of your code if you're still seeing issues after applying this fix!
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@silk moth Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@hollow pollen here is rest of my code , expectAuth: true is already set
import { convexQuery } from "@convex-dev/react-query";
import { createFileRoute, Outlet, redirect } from "@tanstack/react-router";
import { api } from "../../convex/_generated/api";
import { DashboardHeader } from "../components/DashboardHeader";
export const Route = createFileRoute("/_authed")({
component: RouteComponent,
beforeLoad: async ({ context }) => {
if (!context.isAuthenticated) {
throw redirect({ to: "/sign-in" });
}
},
loader: async ({ context, preload }) => {
// Skip preloading this auth-required query when signed out
if (preload && !context.isAuthenticated) return;
await context.queryClient.ensureQueryData(convexQuery(api.auth.getAuthUser, {}));
},
});
function RouteComponent() {
return (
<div>
<DashboardHeader />
<Outlet />
</div>
);
}
import { useMutation } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import { authClient } from "@/lib/auth-client";
export function DashboardHeader() {
const signOutMutation = useMutation({
mutationFn: async () => {
return await authClient.signOut();
},
onSuccess: async () => {
location.reload();
},
});
return (
<header className="sticky top-0 z-40 border-b bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60">
<div className="mx-auto flex h-16 max-w-3xl items-center justify-between px-4 sm:px-6 lg:px-8">
<div className="flex items-center gap-2">
<h1 className="text-2xl font-semibold text-balance text-foreground">Agency Tools</h1>
</div>
<Button variant="ghost" size="icon" onClick={() => signOutMutation.mutate()}>
Sign out
</Button>
</div>
</header>
);
}
Let me check the knowledge base for you again. One moment! :books:
Looking at your code, you already have location.reload() on sign out — that's correct. However, the issue is the order of operations: location.reload() is called in onSuccess, but the useSuspenseQuery for api.auth.getAuthUser may fire an unauthenticated request before the reload completes, because the _authed layout is still mounted with the query active.
The key fix from the migration guide is to navigate/redirect before reloading, so the authenticated route (and its queries) unmount first: [migrate to 0.10]
export const handleSignOut = async () => {
await authClient.signOut({
fetchOptions: {
onSuccess: () => {
location.reload(); // reload triggers redirect to /sign-in via beforeLoad
},
},
});
};
Your current implementation uses useMutation with onSuccess: () => location.reload(), which should work similarly. The reload causes the router to re-evaluate beforeLoad, which checks context.isAuthenticated and redirects to /sign-in before the _authed layout (and its queries) mount.
One thing to check: is context.isAuthenticated being updated correctly before the reload? The reload should reset everything, so this should be fine.
The more likely remaining issue is that your _authed loader has this guard:
loader: async ({ context, preload }) => {
if (preload && !context.isAuthenticated) return;
await context.queryClient.ensureQueryData(convexQuery(api.auth.getAuthUser, {}));
},
This only skips on preload — on a normal load while authenticated, it runs ensureQueryData. After sign-out + reload, beforeLoad should redirect before the loader runs, so this should be fine.
If you're still seeing the error, it may be a timing issue where the query is still subscribed in the TanStack Query cache from before sign-out. The knowledge sources note that with TanStack Query integration, Convex stays subscribed to queries for a while, and when clearAuth is called, all queries refresh — which can cause unauthenticated errors if the component is still mounted. [Roast of convex auth]
Try updating your sign-out to use the pattern from the migration guide directly (using authClient.signOut with fetchOptions.onSuccess), rather than wrapping in useMutation:
import { authClient } from "@/lib/auth-client";
const handleSignOut = async () => {
await authClient.signOut({
fetchOptions: {
onSuccess: () => {
location.reload();
},
},
});
};
This matches exactly what the docs recommend. [migrate to 0.10]
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@silk moth Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@hollow pollen i did this and its still happening
const signOutMutation = useMutation({
mutationFn: async () => {
return await authClient.signOut({
fetchOptions: {
onSuccess: () => {
location.reload();
},
},
});
},
});
Let me check the knowledge base for you again. One moment! :books:
Looking at your code, the location.reload() is inside fetchOptions.onSuccess, which fires after the HTTP sign-out request completes — but the useSuspenseQuery for api.auth.getAuthUser may still be active and re-querying during the brief window between sign-out and reload.
Based on the knowledge sources, there's a known race condition with TanStack Query + Convex auth: when clearAuth is called, all subscribed queries refresh, which can trigger unauthenticated errors if components are still mounted. [Roast of convex auth]
One approach the docs suggest for the _authed route in TanStack Start is to use the AuthBoundary component, which handles auth-related errors gracefully and redirects on unauthenticated state: [Better-auth experimental]
// src/routes/_authed.tsx
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
import { ClientAuthBoundary } from '@/lib/auth-client'
export const Route = createFileRoute("/_authed")({
beforeLoad: ({ context }) => {
if (!context.isAuthenticated) {
throw redirect({ to: "/sign-in" });
}
},
component: () => {
return (
<ClientAuthBoundary>
<Outlet />
</ClientAuthBoundary>
);
},
});
Where ClientAuthBoundary is set up like this:
// lib/auth-client.tsx
"use client";
import { useNavigate } from "@tanstack/react-router";
import { AuthBoundary } from "@convex-dev/better-auth/react";
import { api } from "@/convex/_generated/api";
import { isAuthError } from "@/lib/utils";
import { authClient } from "@/lib/auth-client";
export const ClientAuthBoundary = ({ children }: PropsWithChildren) => {
const navigate = useNavigate();
return (
<AuthBoundary
authClient={authClient}
onUnauth={() => navigate({ to: "/sign-in" })}
getAuthUserFn={api.auth.getAuthUser}
isAuthError={isAuthError}
>
{children}
</AuthBoundary>
);
};
The `AuthBoundary` subscribes to a session-validated user query and handles auth state changes, catching the unauthenticated errors and redirecting instead of crashing. [[Better-auth experimental](<https://labs.convex.dev/better-auth/experimental#authboundary>)]
The knowledge sources don't go deeper into why `location.reload()` alone isn't sufficient in your specific case, so if the `AuthBoundary` approach doesn't resolve it, I'd recommend reaching out to the Convex Discord for further debugging.
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------
@silk moth Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@hollow pollen import { AuthBoundary } from "@convex-dev/better-auth/react";
import { convexQuery } from "@convex-dev/react-query";
import { createFileRoute, Outlet, redirect, useNavigate } from "@tanstack/react-router";
import { ConvexError } from "convex/values";
import type { PropsWithChildren } from "react";
import { authClient } from "@/lib/auth-client";
import { api } from "../../convex/_generated/api";
import { DashboardHeader } from "../components/DashboardHeader";
export const Route = createFileRoute("/_authed")({
component: RouteComponent,
beforeLoad: async ({ context }) => {
if (!context.isAuthenticated) {
throw redirect({ to: "/sign-in" });
}
},
loader: async ({ context, preload }) => {
// Skip preloading this auth-required query when signed out
if (preload && !context.isAuthenticated) return;
await context.queryClient.ensureQueryData(convexQuery(api.auth.getAuthUser, {}));
},
});
function RouteComponent() {
return (
<ClientAuthBoundary>
<DashboardHeader />
<Outlet />
</ClientAuthBoundary>
);
}
export const isAuthError = (error: unknown) => {
if (error instanceof ConvexError) {
const data = error.data;
if (typeof data === "string") {
return /unauthenticated|unauthorized|auth/i.test(data);
}
}
if (error instanceof Error) {
return /unauthenticated|unauthorized/i.test(error.message);
}
return false;
};
export const ClientAuthBoundary = ({ children }: PropsWithChildren) => {
const navigate = useNavigate();
return (
<AuthBoundary
authClient={authClient}
getAuthUserFn={api.auth.getAuthUser}
isAuthError={isAuthError}
onUnauth={() => {
navigate({ to: "/sign-in", replace: true });
}}
>
{children}
</AuthBoundary>
);
};
i did this but its still happening
3/31/2026, 7:12:11 PM [CONVEX Q(auth:getAuthUser)] Uncaught ConvexError: Unauthenticated
Let me check the knowledge base for you again. One moment! :books:
The knowledge sources don't provide a deeper solution beyond what's already been tried (the AuthBoundary approach and location.reload() on sign out). The error you're seeing — Uncaught ConvexError: Unauthenticated on auth:getAuthUser — is a known race condition where Convex queries refresh when auth is cleared, and the query fires before the component unmounts or the redirect completes. [Roast of convex auth]
One thing worth checking based on the knowledge sources: the AuthBoundary is described as experimental [Better-auth experimental], so it may not fully handle all edge cases yet.
The only additional suggestion from the knowledge sources is to ensure the sign-out uses authClient.signOut with fetchOptions.onSuccess: () => location.reload() — which you already have. [migrate to 0.10]
Beyond that, the knowledge sources don't contain enough information to diagnose why the error persists in your specific setup after applying both fixes. I'd recommend:
- Filing a bug or asking in the Convex Discord — this appears to be an edge case in the
@convex-dev/better-auth+ TanStack Start integration that may need a fix at the library level. - As a temporary workaround, you could make
api.auth.getAuthUserreturnnullinstead of throwing when unauthenticated, so the error doesn't surface while the redirect/reload is in flight — though this changes your backend behavior.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@silk moth Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@hollow pollen using this
export const getAuthUserOptional = query({
args: {},
returns: v.union(v.null(), v.any()),
handler: async (ctx) => {
return (await authComponent.safeGetAuthUser(ctx)) ?? null;
},
});
instead of api.auth.getAuthUser was the key to make it work. can you explain
Let me check the knowledge base for you again. One moment! :books:
Great find! The reason this works comes down to the difference between getAuthUser() and safeGetAuthUser().
As noted in the migration docs for @convex-dev/better-auth: [migrate to 0.8]
authComponent.getAuthUser()now throws an error if the user is not found. UseauthComponent.safeGetAuthUser()to match the previous behavior (returningnull).
So the key difference is:
getAuthUser(ctx)→ throws aConvexError: Unauthenticatedif there's no authenticated usersafeGetAuthUser(ctx)→ returnsnullif there's no authenticated user
During sign-out, there's a race condition where Convex queries refresh when auth is cleared, and api.auth.getAuthUser fires briefly while the user is no longer authenticated. With getAuthUser, this throws an error that propagates to the client. With safeGetAuthUser, it simply returns null, which your UI can handle gracefully without crashing. [migrate to 0.8]
Your getAuthUserOptional query:
export const getAuthUserOptional = query({
args: {},
returns: v.union(v.null(), v.any()),
handler: async (ctx) => {
return (await authComponent.safeGetAuthUser(ctx)) ?? null;
},
});