SERVER_ERROR on /verify-email: Cannot read properties of undefined (reading 'name') #1852

Closed
opened 2026-03-13 09:08:21 -05:00 by GiteaMirror · 3 comments
Owner

Originally created by @muamarhm on GitHub (Sep 5, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

🔄 Steps to Reproduce

Sign up with email & password (email verification is enabled)
Receive the verification email
Click the verification link (e.g., /api/auth/verify-email?token=...)
Server returns 500 Internal Server Error

Current vs. Expected behavior

⚠️ Current Behavior

Verification link is valid and not expired

But on clicking, the API responds with:
SERVER_ERROR: [TypeError: Cannot read properties of undefined (reading 'name')]

Console logs show:
GET /api/auth/verify-email?...&callbackURL=/dashboard 500 in 746ms
Stack trace points to access of user.name somewhere post-verification.

Expected Behavior

User should be verified successfully
Should be automatically signed in (autoSignIn: true)
Should be redirected to /dashboard or the passed callbackURL without error

What version of Better Auth are you using?

^1.3.7

System info

✅ Environment

better-auth: ^1.3.7
Runtime: Bun 1.2.21
Framework: Next.js 15.5.2

Which area(s) are affected? (Select all that apply)

Backend

Auth config (if applicable)

import { betterAuth } from 'better-auth'
import { createAuthMiddleware } from 'better-auth/api'
import { nextCookies } from 'better-auth/next-js'
import { customSession, emailOTP, multiSession } from 'better-auth/plugins'
import { format } from 'date-fns'
import { createPool } from 'mysql2/promise'
import { sendEmail } from './nodemailer'
import { notificationQueue } from './queue'

const pool = createPool({
	host: process.env.DB_HOST!,
	user: process.env.DB_USER!,
	password: process.env.DB_PASSWORD!,
	database: process.env.DB_NAME!,
	waitForConnections: true,
	connectionLimit: 10,
	queueLimit: 0,
})

const bullMqActivate = process.env.BULL_MQ! === '0' ? false : true

export const auth = betterAuth({
	appName: 'Muam Starter App',
	baseURL: process.env.NEXT_PUBLIC_APP_URL!,
	basePath: '/api/auth',
	trustedOrigins: async () => {
		// Dynamic Origins
		const trustedOriginList = process.env.TRUSTED_ORIGINS!
			? process.env.TRUSTED_ORIGINS.split(',').map((origin) => origin.trim())
			: []
		return trustedOriginList
	},
	database: pool,
	emailAndPassword: {
		enabled: true,
		disableSignUp: false,
		requireEmailVerification: true,
		minPasswordLength: 8,
		maxPasswordLength: 128,
		autoSignIn: true,
		sendResetPassword: async ({ user, url, token }) => {
			// Send reset password email
			const delay = 3000
			const job_id = format(new Date(), 'yyyy-MM-dd HH:mm:ss')
			const payload = {
				job_name: 'send reset password',
				email: user.email,
			}
			console.log('sendResetPassword email.', { ...user, url, token })
			console.log({ ...payload, job_id })

			if (bullMqActivate) {
				await notificationQueue.add(job_id, payload, {
					delay,
					removeOnComplete: true,
					removeOnFail: false,
				})
			} else {
				// await sendEmail(payload)
			}
		},
		onPasswordReset: async ({ user }, request) => {
			// your logic here
			console.log(`Password for user ${user.email} has been reset.`)
		},
		resetPasswordTokenExpiresIn: 3600, // 1 hour
	},
	emailVerification: {
		sendOnSignUp: true,
		autoSignInAfterVerification: true,
		expiresIn: 3600, // 1 hour
		sendVerificationEmail: async ({ user, url, token }) => {
			// Send verification email to user
			if (bullMqActivate) {
				const delay = 3000
				const job_id = format(new Date(), 'yyyy-MM-dd HH:mm:ss')
				const payload = {
					job_name: 'send reset password',
					to: user.email,
					subject: 'Verify your email address',
					text: `Click the link to verify your email: ${url}`,
					url,
					token,
				}
				console.log('send verifiation email.', payload)
				console.log({ ...payload, job_id })
				await notificationQueue.add(job_id, payload, {
					delay,
					removeOnComplete: true,
					removeOnFail: false,
				})
			} else {
				await sendEmail(
					user.email,
					'Verify your email address',
					`Click the link to verify your email: ${url}`,
					`<p><a href="${url}">verify your email</a></p>`,
				)
			}
		},
	},
	socialProviders: {
		google: {
			clientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID as string,
			clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
		},
	},
	plugins: [
		nextCookies(),
		customSession(async ({ user, session }) => {
			if (!user) {
				console.warn('[CUSTOM SESSION] User is undefined')
				return { user: null, session }
			}

			return {
				user: {
					...user,
					// roles: 'admin',
				},
				session,
			}
		}),

		emailOTP({
			sendVerificationOTP: async ({ email, otp, type }) => {
				if (type === 'sign-in') {
					// Send the OTP for sign in
				} else if (type === 'email-verification') {
					// Send the OTP for email verification
				} else {
					// Send the OTP for password reset
				}
			},
		}),
		multiSession({
			maximumSessions: 3,
		}),
	],
	rateLimit: {
		enabled: true,
		window: 10,
		max: 100,
		storage: 'memory',
		modelName: 'rateLimit',
	},
	logger: {
		level: 'info',
		log: (level, message, ...args) => {
			// Send logs to a custom logging service
			console.log({
				level,
				message,
				metadata: args,
				timestamp: new Date().toISOString(),
			})
		},
	},
	databaseHooks: {
		user: {
			create: {
				before: async (user) => {
					// Modify user data before creation
					// return { data: { ...user, customField: 'value' } }
				},
				after: async (user) => {
					// Perform actions after user creation
				},
			},
			update: {
				before: async (userData) => {
					// Modify user data before update
					// return { data: { ...userData, updatedAt: new Date() } }
				},
				after: async (user) => {
					// Perform actions after user update
				},
			},
			session: {
				// Session hooks
			},
			account: {
				// Account hooks
			},
			verification: {
				after: async ({ user }: any) => {
					console.log('[✅ VERIFICATION COMPLETE]', user)
				},
			},
		},
	},
	onAPIError: {
		throw: true,
		onError: (error, ctx) => {
			console.error('Auth error:', error)
		},
		errorURL: '/auth/error',
	},

	hooks: {
		before: createAuthMiddleware(async (ctx) => {
			// Execute before processing the request
			console.log('Request path:', ctx.path)
		}),
		after: createAuthMiddleware(async (ctx) => {
			// Execute after processing the request
			console.log('Response:', ctx.context.returned)
		}),
	},
	telemetry: {
		enabled: false,
	},
})

Additional context

Image
Originally created by @muamarhm on GitHub (Sep 5, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 🔄 Steps to Reproduce Sign up with email & password (email verification is enabled) Receive the verification email Click the verification link (e.g., /api/auth/verify-email?token=...) Server returns 500 Internal Server Error ### Current vs. Expected behavior ⚠️ Current Behavior Verification link is valid and not expired But on clicking, the API responds with: SERVER_ERROR: [TypeError: Cannot read properties of undefined (reading 'name')] Console logs show: GET /api/auth/verify-email?...&callbackURL=/dashboard 500 in 746ms Stack trace points to access of user.name somewhere post-verification. ✅ Expected Behavior User should be verified successfully Should be automatically signed in (autoSignIn: true) Should be redirected to /dashboard or the passed callbackURL without error ### What version of Better Auth are you using? ^1.3.7 ### System info ```bash ✅ Environment better-auth: ^1.3.7 Runtime: Bun 1.2.21 Framework: Next.js 15.5.2 ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { betterAuth } from 'better-auth' import { createAuthMiddleware } from 'better-auth/api' import { nextCookies } from 'better-auth/next-js' import { customSession, emailOTP, multiSession } from 'better-auth/plugins' import { format } from 'date-fns' import { createPool } from 'mysql2/promise' import { sendEmail } from './nodemailer' import { notificationQueue } from './queue' const pool = createPool({ host: process.env.DB_HOST!, user: process.env.DB_USER!, password: process.env.DB_PASSWORD!, database: process.env.DB_NAME!, waitForConnections: true, connectionLimit: 10, queueLimit: 0, }) const bullMqActivate = process.env.BULL_MQ! === '0' ? false : true export const auth = betterAuth({ appName: 'Muam Starter App', baseURL: process.env.NEXT_PUBLIC_APP_URL!, basePath: '/api/auth', trustedOrigins: async () => { // Dynamic Origins const trustedOriginList = process.env.TRUSTED_ORIGINS! ? process.env.TRUSTED_ORIGINS.split(',').map((origin) => origin.trim()) : [] return trustedOriginList }, database: pool, emailAndPassword: { enabled: true, disableSignUp: false, requireEmailVerification: true, minPasswordLength: 8, maxPasswordLength: 128, autoSignIn: true, sendResetPassword: async ({ user, url, token }) => { // Send reset password email const delay = 3000 const job_id = format(new Date(), 'yyyy-MM-dd HH:mm:ss') const payload = { job_name: 'send reset password', email: user.email, } console.log('sendResetPassword email.', { ...user, url, token }) console.log({ ...payload, job_id }) if (bullMqActivate) { await notificationQueue.add(job_id, payload, { delay, removeOnComplete: true, removeOnFail: false, }) } else { // await sendEmail(payload) } }, onPasswordReset: async ({ user }, request) => { // your logic here console.log(`Password for user ${user.email} has been reset.`) }, resetPasswordTokenExpiresIn: 3600, // 1 hour }, emailVerification: { sendOnSignUp: true, autoSignInAfterVerification: true, expiresIn: 3600, // 1 hour sendVerificationEmail: async ({ user, url, token }) => { // Send verification email to user if (bullMqActivate) { const delay = 3000 const job_id = format(new Date(), 'yyyy-MM-dd HH:mm:ss') const payload = { job_name: 'send reset password', to: user.email, subject: 'Verify your email address', text: `Click the link to verify your email: ${url}`, url, token, } console.log('send verifiation email.', payload) console.log({ ...payload, job_id }) await notificationQueue.add(job_id, payload, { delay, removeOnComplete: true, removeOnFail: false, }) } else { await sendEmail( user.email, 'Verify your email address', `Click the link to verify your email: ${url}`, `<p><a href="${url}">verify your email</a></p>`, ) } }, }, socialProviders: { google: { clientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID as string, clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, }, }, plugins: [ nextCookies(), customSession(async ({ user, session }) => { if (!user) { console.warn('[CUSTOM SESSION] User is undefined') return { user: null, session } } return { user: { ...user, // roles: 'admin', }, session, } }), emailOTP({ sendVerificationOTP: async ({ email, otp, type }) => { if (type === 'sign-in') { // Send the OTP for sign in } else if (type === 'email-verification') { // Send the OTP for email verification } else { // Send the OTP for password reset } }, }), multiSession({ maximumSessions: 3, }), ], rateLimit: { enabled: true, window: 10, max: 100, storage: 'memory', modelName: 'rateLimit', }, logger: { level: 'info', log: (level, message, ...args) => { // Send logs to a custom logging service console.log({ level, message, metadata: args, timestamp: new Date().toISOString(), }) }, }, databaseHooks: { user: { create: { before: async (user) => { // Modify user data before creation // return { data: { ...user, customField: 'value' } } }, after: async (user) => { // Perform actions after user creation }, }, update: { before: async (userData) => { // Modify user data before update // return { data: { ...userData, updatedAt: new Date() } } }, after: async (user) => { // Perform actions after user update }, }, session: { // Session hooks }, account: { // Account hooks }, verification: { after: async ({ user }: any) => { console.log('[✅ VERIFICATION COMPLETE]', user) }, }, }, }, onAPIError: { throw: true, onError: (error, ctx) => { console.error('Auth error:', error) }, errorURL: '/auth/error', }, hooks: { before: createAuthMiddleware(async (ctx) => { // Execute before processing the request console.log('Request path:', ctx.path) }), after: createAuthMiddleware(async (ctx) => { // Execute after processing the request console.log('Response:', ctx.context.returned) }), }, telemetry: { enabled: false, }, }) ``` ### Additional context <img width="1015" height="400" alt="Image" src="https://github.com/user-attachments/assets/cd2f3bae-70ec-4512-88e3-0aed0d1a6e69" />
GiteaMirror added the nextjsbug labels 2026-03-13 09:08:22 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Sep 5, 2025):

Hi @muamarhm! I'm Dosu and I’m helping the better-auth team.

This error is likely caused by the user object being undefined or missing required fields (like name) during or after the email verification flow. In your config, the customSession plugin returns { user: null, session } if user is undefined, which can propagate a null user object downstream and trigger this error if any code assumes user is always defined and has a name property.

To fix this, add defensive checks wherever user properties are accessed—especially in customSession, hooks, and any post-verification logic. Make sure your customSession callback always returns a fully populated user object or handles the null case gracefully. Also, check that your database and hooks always return a complete user record after verification.

Recent test improvements in Better Auth confirm that with autoSignInAfterVerification enabled, the user session is set and emailVerified is true after verification, as long as the user object is present and correct (source). If you have custom hooks or plugins, ensure they don't strip or nullify the user object.

Add logging before returning from customSession and in your verification hooks to confirm the shape of the user object at each step. This should help you pinpoint where user becomes undefined or incomplete.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

@dosubot[bot] commented on GitHub (Sep 5, 2025): <!-- Greeting --> Hi @muamarhm! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> This error is likely caused by the user object being undefined or missing required fields (like name) during or after the email verification flow. In your config, the customSession plugin returns `{ user: null, session }` if user is undefined, which can propagate a null user object downstream and trigger this error if any code assumes user is always defined and has a name property. To fix this, add defensive checks wherever user properties are accessed—especially in customSession, hooks, and any post-verification logic. Make sure your customSession callback always returns a fully populated user object or handles the null case gracefully. Also, check that your database and hooks always return a complete user record after verification. Recent test improvements in Better Auth confirm that with `autoSignInAfterVerification` enabled, the user session is set and `emailVerified` is true after verification, as long as the user object is present and correct ([source](https://github.com/better-auth/better-auth/pull/3746)). If you have custom hooks or plugins, ensure they don't strip or nullify the user object. Add logging before returning from customSession and in your verification hooks to confirm the shape of the user object at each step. This should help you pinpoint where user becomes undefined or incomplete. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/17ad37d3-a9c9-46fc-aee4-e6f91070b040?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/17ad37d3-a9c9-46fc-aee4-e6f91070b040?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/17ad37d3-a9c9-46fc-aee4-e6f91070b040?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/17ad37d3-a9c9-46fc-aee4-e6f91070b040?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/17ad37d3-a9c9-46fc-aee4-e6f91070b040?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/17ad37d3-a9c9-46fc-aee4-e6f91070b040?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/17ad37d3-a9c9-46fc-aee4-e6f91070b040?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/4458)
Author
Owner

@Kinfe123 commented on GitHub (Sep 5, 2025):

are you returning a valid user object in customSession ? can you check that ?

@Kinfe123 commented on GitHub (Sep 5, 2025): are you returning a valid user object in customSession ? can you check that ?
Author
Owner

@muamarhm commented on GitHub (Sep 5, 2025):

@Kinfe123 even if I don't use customSession, it's still the same

Image
@muamarhm commented on GitHub (Sep 5, 2025): @Kinfe123 even if I don't use customSession, it's still the same <img width="1895" height="1015" alt="Image" src="https://github.com/user-attachments/assets/5c38792e-c48f-497f-9f83-3a627cff8be2" />
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#1852