
You've just pushed your Next.js app to Vercel. Everything works perfectly locally—authentication flows are smooth, your layouts render correctly, and the build completes without a hitch. Then Vercel hits you with:
Error occurred prerendering page "/_not-found"
TypeError: Cannot read properties of null (reading 'auth')
This Next.js prerendering error for the _not-found page with auth is one of those frustrating issues that only surfaces in production. It passes all local checks but bombs on Vercel's static generation phase. Let's break down exactly why this happens and how to fix it properly.
The error message is straightforward: somewhere in your code, you're trying to access .auth (or a nested property) on something that's null. The tricky part is when this happens.
Here's a typical scenario. You have a layout that fetches the current user session and passes auth data to child components:
// ❌ Broken: app/layout.tsx
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { Header }
Content creator and developer at UICraft Marketplace, sharing insights and tutorials on modern web development.
Save hours of development time with our premium Next.js templates. Built with Next.js 16, React 19, and Tailwind CSS 4.
Get the latest articles, tutorials, and product updates delivered to your inbox.
Locally, this works because:
But on Vercel during the build phase, Next.js prerenders the /_not-found page. There's no active request context, no cookies, no session—just static HTML generation. Your getServerSession() returns null, and null.user.name throws the TypeError.
The build output typically looks like this:
✓ Linting and checking validity of types
✓ Collecting page data
Generating static pages (0/8) [= ]
Error occurred prerendering page "/_not-found".
Read more: https://nextjs.org/docs/messages/prerender-error
TypeError: Cannot read properties of null (reading 'auth')
at RootLayout (/app/.next/server/app/layout.js:1:2345)
at renderToHTML (...)
✓ Generating static pages (8/8)
> Export encountered errors on following paths:
/_not-found/page: /_not-found
This issue has been documented in the Next.js community, including discussions on GitHub issue #65447, where developers encounter similar prerendering failures with various module access patterns.
To understand this bug, you need to understand how Next.js handles the _not-found page.
In the App Router, not-found.tsx is a special file that renders when notFound() is called or when a route doesn't match. During the build process, Next.js prerenders this page to create a static fallback for 404 responses.
Here's the key insight: prerendering happens at build time, not request time. There are:
Any code that assumes a valid session during render will fail.
The _not-found page inherits your root layout. If that layout contains auth-dependent logic without null checks, the prerender fails:
Request Flow (Runtime):
User → Headers/Cookies → getServerSession() → session object → render
Build Flow (Static):
Build process → No context → getServerSession() → null → 💥 crash
This is the fundamental discrepancy between local development (always dynamic) and Vercel production builds (static where possible).
Pattern 1: Direct session property access
// ❌ Assumes session always exists
const session = await getServerSession(authOptions);
const userId = session.user.id;Pattern 2: Auth hooks without guards
// ❌ Custom hook that doesn't handle null
export function useAuth() {
const session = useSession();
return session.data.user; // Crashes if session.data is null
}Pattern 3: Nested layout auth assumptions
// ❌ Layout assumes authenticated users only
export default async function DashboardLayout({ children }) {
const session = await getServerSession(authOptions);
const permissions = await getPermissions(session.user.id); // 💥
// ...
}The fix involves defensive coding patterns that account for null session states during static generation. Here are the approaches, ordered by preference.
The simplest fix is adding optional chaining (?.) to all session property access:
// ✅ Fixed: app/layout.tsx
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { Header } from "@/components/Header";
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await getServerSession(authOptions);
return (
<html lang="en">
<body>
<Header
userName={session?.user?.name ?? "Guest"}
userRole={session?.user?.role ?? "visitor"}
/>
{children}
</body>
</html>
);
}The ?. operator short-circuits when it encounters null or undefined, and the ?? nullish coalescing operator provides sensible defaults.
For more complex auth-dependent UI, use explicit null checks:
// ✅ Fixed: components/Header.tsx
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
export async function Header() {
const session = await getServerSession(authOptions);
// Guard clause: handle unauthenticated state
if (!session) {
return (
<header className="header">
<nav>
<a href="/login">Sign In</a>
<a href="/register">Sign Up</a>
</nav>
</header>
);
}
// Safe to access session properties after the guard
return (
<header className="header">
<nav>
<span>Welcome, {session.user.name}</span>
<UserMenu user={session.user} />
</nav>
</header>
);
}This pattern is clearer about intent and easier to maintain than scattered optional chaining.
For large codebases, centralize the null handling:
// ✅ lib/safe-auth.ts
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
export type SafeSession = {
isAuthenticated: boolean;
user: {
id: string;
name: string;
email: string;
role: string;
} | null;
};
export async function getSafeSession(): Promise<SafeSession> {
const session = await getServerSession(authOptions);
if (!session?.user) {
return {
isAuthenticated: false,
user: null,
};
}
return {
isAuthenticated: true,
user: {
id: session.user.id ?? "",
name: session.user.name ?? "Anonymous",
email: session.user.email ?? "",
role: session.user.role ?? "user",
},
};
}Now all components use this helper:
// ✅ app/layout.tsx
import { getSafeSession } from "@/lib/safe-auth";
import { Header } from "@/components/Header";
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const { isAuthenticated, user } = await getSafeSession();
return (
<html lang="en">
<body>
<Header isAuthenticated={isAuthenticated} user={user} />
{children}
</body>
</html>
);
}If certain layouts require authentication context to function, force dynamic rendering:
// ✅ app/(dashboard)/layout.tsx
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { redirect } from "next/navigation";
// Force dynamic rendering - skip prerendering
export const dynamic = "force-dynamic";
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await getServerSession(authOptions);
if (!session) {
redirect("/login");
}
// Now session is guaranteed to exist or user is redirected
return (
<div className="dashboard">
<Sidebar user={session.user} />
<main>{children}</main>
</div>
);
}With export const dynamic = 'force-dynamic', Next.js won't attempt to prerender these routes, avoiding the null session issue entirely.
Sometimes you need a dedicated not-found.tsx that handles the unauthenticated prerender case:
// ✅ app/not-found.tsx
import Link from "next/link";
// This page will be prerendered - no auth assumptions allowed
export default function NotFound() {
return (
<div className="not-found-container">
<h1>404 - Page Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
<Link href="/">Return Home</Link>
</div>
);
}Keep your not-found.tsx free of any auth-dependent logic. It will be prerendered statically and served as a fallback.
Encountering this Next.js prerendering error once is enough. Here's how to prevent it from happening again.
Search your codebase for patterns that assume session existence:
# Find direct session property access (potential issues)
grep -r "session\." --include="*.tsx" --include="*.ts" | grep -v "session?"Every session. should probably be session?. unless it's in a protected route with dynamic rendering.
Ensure your tsconfig.json has strict mode enabled:
{
"compilerOptions": {
"strict": true,
"strictNullChecks": true
}
}TypeScript will then flag potential null access at compile time:
const session = await getServerSession(authOptions);
const name = session.user.name; // ❌ TypeScript Error: Object is possibly 'null'
const name = session?.user?.name; // ✅ No errorStructure your app to isolate auth-required sections:
app/
├── (public)/
│ ├── layout.tsx # No auth assumptions
│ ├── page.tsx # Landing page
│ └── about/
│ └── page.tsx
├── (protected)/
│ ├── layout.tsx # Auth required, force-dynamic
│ ├── dashboard/
│ └── settings/
├── layout.tsx # Root: safe auth with fallbacks
└── not-found.tsx # No auth at all
Create wrapper components that handle the auth state:
// ✅ components/AuthGuard.tsx
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
interface AuthGuardProps {
children: React.ReactNode;
fallback?: React.ReactNode;
}
export async function AuthGuard({ children, fallback = null }: AuthGuardProps) {
const session = await getServerSession(authOptions);
if (!session) {
return <>{fallback}</>;
}
return <>{children}</>;
}
// Usage in layout
export default function Layout({ children }) {
return (
<html>
<body>
<AuthGuard fallback={<PublicHeader />}>
<AuthenticatedHeader />
</AuthGuard>
{children}
</body>
</html>
);
}Before pushing to Vercel, run the production build locally:
npm run buildThis runs the same static generation process that Vercel uses. Prerendering errors will surface here, saving you from failed deployments.
Create a coding standard for auth handling:
// DOC: Auth Access Guidelines
//
// ❌ NEVER: Direct property access without null check
// const userId = session.user.id;
//
// ✅ ALWAYS: Use optional chaining or guards
// const userId = session?.user?.id;
// if (!session) return <UnauthenticatedView />;
//
// 💡 PREFER: Use getSafeSession() helper for consistent handling_not-found page is prerendered at build time with no request context, cookies, or session data—any auth access will be null.?.) and nullish coalescing (??) are your first line of defense for auth property access.force-dynamic for layouts that truly require auth, accepting that these routes won't benefit from static optimization.npm run build locally before deploying to catch prerendering errors early.Audit your codebase: Search for session. without optional chaining and getServerSession calls without null guards.
Create a getSafeSession() helper: Centralize your auth logic with proper typing and fallback values.
Review your layout hierarchy: Ensure your root layout and not-found.tsx don't assume authenticated state.
Add build testing to CI: Include npm run build in your pipeline to catch these errors before they hit production.
Read the related discussions: Check out GitHub issue #65447 for additional context and edge cases the community has encountered.
The pattern of "works locally, fails on Vercel" is almost always about the difference between dynamic and static rendering. Once you internalize that prerendering has no request context, these bugs become easy to spot and prevent.
Running `next lint` with ESLint 9 throws cryptic "Unknown options" errors for useEslintrc and extensions. This guide explains why ESLint 9's flat config breaks Next.js linting and provides clear solutions to get your project working again.

The "Invalid Unicode Code Point" Terser error during Next.js builds is a frustrating Docker-specific issue. This guide explains why Alpine Linux triggers this minification failure and provides proven solutions to get your builds working.