Sentry is not capturing 500 errors inside Better Auth #1580

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

Originally created by @brunowego on GitHub (Jul 25, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Create a Next.js app using better-auth inside a packages/auth directory
  2. Configure onAPIError with throw: true and a custom onError function that calls captureException(error) from @sentry/nextjs
  3. Trigger a server-side error within Better Auth (e.g., throw inside a plugin or hook)
  4. The error causes a 500, but it is not reported to Sentry

Current vs. Expected behavior

Expected:

Errors thrown inside Better Auth and handled via onAPIError should be captured and sent to Sentry using captureException.

Current:

500 errors thrown internally by Better Auth are not captured by Sentry — no events appear in the Sentry dashboard. This results in undetected critical failures.

What version of Better Auth are you using?

1.3.1

Provide environment information

- OS: macOS Sonoma
- Browser: Chrome (irrelevant in this case, error is backend-related)
- Next.js: 14.x (App Router)
- Hosting: Vercel
- Sentry SDK: @sentry/nextjs@7.x

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

Backend

Auth config (if applicable)

import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { admin, organization, oneTap } from 'better-auth/plugins'
import { v4 as uuidv4 } from 'uuid'
import { captureException } from '@sentry/nextjs'
import { type BetterAuthOptions, betterAuth } from 'better-auth'
import { cache } from 'react'
import { headers } from 'next/headers'

import db from '@acme/db'
import { sendEmail, EmailTemplate } from '@acme/email'

import senv from './lib/env/server'
import penv from './lib/env/client'
import { ac, roles } from './permissions'

export const config = {
  database: drizzleAdapter(db, {
    provider: 'pg',
    usePlural: true,
  }),
  socialProviders: {
    ...(senv.GOOGLE_CLIENT_ID && senv.GOOGLE_CLIENT_SECRET
      ? {
          google: {
            clientId: senv.GOOGLE_CLIENT_ID,
            clientSecret: senv.GOOGLE_CLIENT_SECRET,
            disableSignUp: senv.DISABLE_SIGNUP,
            mapProfileToUser: (profile) => {
              return {
                ...profile,
                firstName: profile.given_name,
                lastName: profile.family_name,
                image: profile.picture?.replace('=s96-c', '=s256-c'),
              }
            },
          },
        }
      : {}),
  },
  plugins: [
    admin({
      defaultRole: 'user',
      adminRoles: ['admin'],
    }),
    organization({
      ac,
      roles,
      sendInvitationEmail: async ({
        email,
        organization,
        inviter,
        invitation,
      }) => {
        await sendEmail(email, EmailTemplate.MemberInviteEmail, {
          workspaceName: organization.name,
          invitedByName: inviter.user.name,
          invitedByEmail: inviter.user.email,
          name: email,
          inviteLink: `${penv.NEXT_PUBLIC_APP_URL}/sign-in?redirectTo=/accept-invitation/${invitation.id}`,
        })
      },
    }),
    oneTap(),
  ],
  user: {
    additionalFields: {
      firstName: {
        type: 'string',
        fieldName: 'firstName',
      },
      lastName: {
        type: 'string',
        required: false,
        fieldName: 'lastName',
      },
      theme: {
        type: 'string',
        required: false,
        fieldName: 'theme',
      },
      role: {
        type: 'string',
        required: false,
        fieldName: 'role',
      },
    },
  },
  session: {
    expiresIn: 60 * 60 * 24 * 7, // 7 days
    updateAge: 60 * 60 * 24, // 1 day
  },
  advanced: {
    cookiePrefix: 'acme',
    database: {
      generateId: () => uuidv4(),
    },
  },
  databaseHooks: {
    user: {
      create: {
        before: async (user) => {
          if (user.email.endsWith('@acme.com')) {
            return {
              data: {
                ...user,
                role: 'admin',
              },
            }
          }

          return {
            data: {
              ...user,
            },
          }
        },
      },
    },
  },
  onAPIError: {
    throw: true,
    onError: (error) => {
      captureException(error, {
        tags: {
          'better-auth.error': 'true',
        },
      })
    },
  },
} satisfies BetterAuthOptions

const auth = betterAuth(config)

const getCurrentSession = cache(
  async () => await auth.api.getSession({ headers: await headers() }),
)

export { auth as default, getCurrentSession }

Additional context

  • The Better Auth configuration is isolated in a monorepo package (packages/auth)
  • Errors are thrown, the client receives a 500 response, but Sentry does not receive any error report
  • This happens both in development and in production
  • Other parts of the app do capture Sentry errors correctly — only Better Auth-related errors are missed
  • Possible cause: the internal throw may happen outside the current execution context or not propagate properly
Originally created by @brunowego on GitHub (Jul 25, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Create a Next.js app using better-auth inside a packages/auth directory 2. Configure onAPIError with throw: true and a custom onError function that calls captureException(error) from @sentry/nextjs 3. Trigger a server-side error within Better Auth (e.g., throw inside a plugin or hook) 4. The error causes a 500, but it is not reported to Sentry ### Current vs. Expected behavior Expected: Errors thrown inside Better Auth and handled via onAPIError should be captured and sent to Sentry using captureException. Current: 500 errors thrown internally by Better Auth are not captured by Sentry — no events appear in the Sentry dashboard. This results in undetected critical failures. ### What version of Better Auth are you using? 1.3.1 ### Provide environment information ```bash - OS: macOS Sonoma - Browser: Chrome (irrelevant in this case, error is backend-related) - Next.js: 14.x (App Router) - Hosting: Vercel - Sentry SDK: @sentry/nextjs@7.x ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { drizzleAdapter } from 'better-auth/adapters/drizzle' import { admin, organization, oneTap } from 'better-auth/plugins' import { v4 as uuidv4 } from 'uuid' import { captureException } from '@sentry/nextjs' import { type BetterAuthOptions, betterAuth } from 'better-auth' import { cache } from 'react' import { headers } from 'next/headers' import db from '@acme/db' import { sendEmail, EmailTemplate } from '@acme/email' import senv from './lib/env/server' import penv from './lib/env/client' import { ac, roles } from './permissions' export const config = { database: drizzleAdapter(db, { provider: 'pg', usePlural: true, }), socialProviders: { ...(senv.GOOGLE_CLIENT_ID && senv.GOOGLE_CLIENT_SECRET ? { google: { clientId: senv.GOOGLE_CLIENT_ID, clientSecret: senv.GOOGLE_CLIENT_SECRET, disableSignUp: senv.DISABLE_SIGNUP, mapProfileToUser: (profile) => { return { ...profile, firstName: profile.given_name, lastName: profile.family_name, image: profile.picture?.replace('=s96-c', '=s256-c'), } }, }, } : {}), }, plugins: [ admin({ defaultRole: 'user', adminRoles: ['admin'], }), organization({ ac, roles, sendInvitationEmail: async ({ email, organization, inviter, invitation, }) => { await sendEmail(email, EmailTemplate.MemberInviteEmail, { workspaceName: organization.name, invitedByName: inviter.user.name, invitedByEmail: inviter.user.email, name: email, inviteLink: `${penv.NEXT_PUBLIC_APP_URL}/sign-in?redirectTo=/accept-invitation/${invitation.id}`, }) }, }), oneTap(), ], user: { additionalFields: { firstName: { type: 'string', fieldName: 'firstName', }, lastName: { type: 'string', required: false, fieldName: 'lastName', }, theme: { type: 'string', required: false, fieldName: 'theme', }, role: { type: 'string', required: false, fieldName: 'role', }, }, }, session: { expiresIn: 60 * 60 * 24 * 7, // 7 days updateAge: 60 * 60 * 24, // 1 day }, advanced: { cookiePrefix: 'acme', database: { generateId: () => uuidv4(), }, }, databaseHooks: { user: { create: { before: async (user) => { if (user.email.endsWith('@acme.com')) { return { data: { ...user, role: 'admin', }, } } return { data: { ...user, }, } }, }, }, }, onAPIError: { throw: true, onError: (error) => { captureException(error, { tags: { 'better-auth.error': 'true', }, }) }, }, } satisfies BetterAuthOptions const auth = betterAuth(config) const getCurrentSession = cache( async () => await auth.api.getSession({ headers: await headers() }), ) export { auth as default, getCurrentSession } ``` ### Additional context - The Better Auth configuration is isolated in a monorepo package (packages/auth) - Errors are thrown, the client receives a 500 response, but Sentry does not receive any error report - This happens both in development and in production - Other parts of the app do capture Sentry errors correctly — only Better Auth-related errors are missed - Possible cause: the internal throw may happen outside the current execution context or not propagate properly
Author
Owner

@dosubot[bot] commented on GitHub (Jul 25, 2025):

This happens because with onAPIError: { throw: true }, Better Auth calls your onError handler (where you call captureException), then immediately re-throws the error. In Next.js App Router, if the error is not caught by a boundary or middleware that Sentry hooks into, Sentry may not capture it—even if you call captureException—because the process may terminate before Sentry can flush the event, or the error is thrown outside Sentry's context reference.

Recent changes in Better Auth introduced an APIError class and recommend catching these errors in your own server code reference. To ensure Sentry always captures these errors, wrap your Better Auth API calls in a try/catch at the route or handler level and call captureException there. For example:

import { APIError } from "better-auth/api";
import { captureException } from "@sentry/nextjs";

export async function POST(req: Request) {
  try {
    // your Better Auth call here
  } catch (error) {
    if (error instanceof APIError) {
      captureException(error, { tags: { "better-auth.error": "true" } });
    }
    throw error; // Let Next.js handle the 500
  }
}

This ensures Sentry gets the error in the right context. You can still use onAPIError for custom logging, but for Sentry, catching at the route level is more reliable in Next.js App Router.

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 (Jul 25, 2025): <!-- Answer --> This happens because with `onAPIError: { throw: true }`, Better Auth calls your `onError` handler (where you call `captureException`), then immediately re-throws the error. In Next.js App Router, if the error is not caught by a boundary or middleware that Sentry hooks into, Sentry may not capture it—even if you call `captureException`—because the process may terminate before Sentry can flush the event, or the error is thrown outside Sentry's context [reference](https://github.com/better-auth/better-auth/blob/b5a4c3fc783e919db4e8150de622a947959dd222/packages/better-auth/src/api/index.ts). Recent changes in Better Auth introduced an `APIError` class and recommend catching these errors in your own server code [reference](https://github.com/better-auth/better-auth/pull/101). To ensure Sentry always captures these errors, wrap your Better Auth API calls in a try/catch at the route or handler level and call `captureException` there. For example: ```ts import { APIError } from "better-auth/api"; import { captureException } from "@sentry/nextjs"; export async function POST(req: Request) { try { // your Better Auth call here } catch (error) { if (error instanceof APIError) { captureException(error, { tags: { "better-auth.error": "true" } }); } throw error; // Let Next.js handle the 500 } } ``` This ensures Sentry gets the error in the right context. You can still use `onAPIError` for custom logging, but for Sentry, catching at the route level is more reliable in Next.js App Router. <!-- 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/db614912-4a7b-424e-80fe-4386b6f7c309?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/db614912-4a7b-424e-80fe-4386b6f7c309?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/db614912-4a7b-424e-80fe-4386b6f7c309?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/db614912-4a7b-424e-80fe-4386b6f7c309?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/db614912-4a7b-424e-80fe-4386b6f7c309?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/db614912-4a7b-424e-80fe-4386b6f7c309?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/db614912-4a7b-424e-80fe-4386b6f7c309?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/3617)
Author
Owner

@ping-maxwell commented on GitHub (Aug 11, 2025):

It's most likely that the error isn't getting caught by onAPIError and thus sentry can't capture it.
This is an existing issue, so I will be closing this.
If you console.log the onAPIError and do see a response but Sentry isn't capturing it, please re-open this issue.

@ping-maxwell commented on GitHub (Aug 11, 2025): It's most likely that the error isn't getting caught by onAPIError and thus sentry can't capture it. This is an existing issue, so I will be closing this. If you console.log the onAPIError and do see a response but Sentry isn't capturing it, please re-open this issue.
Author
Owner

@ping-maxwell commented on GitHub (Aug 11, 2025):

https://github.com/better-auth/better-auth/issues/2400#issuecomment-3009333079

@ping-maxwell commented on GitHub (Aug 11, 2025): https://github.com/better-auth/better-auth/issues/2400#issuecomment-3009333079
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#1580