OTP plugin: handle exceptions from sendVerificationOTP() to give more control over the flow #993

Closed
opened 2026-03-13 08:16:11 -05:00 by GiteaMirror · 1 comment
Owner

Originally created by @vas3k on GitHub (Apr 7, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Configure a better-auth instance to use OTP codes for sign-ins with a custom logic for checking that user exists and not banned on a server.
  plugins: [
    emailOTP({
      disableSignUp: true,
      sendVerificationOTP: async ({ email, otp }) => {
        const user = await getUserByEmail(email)
        if (!user) {
          // THIS WORKS BUT DOES NOT RETURN THE ERROR FROM SERVER
          throw new Error("User with this email does not exist")
        }
        await sendOTPCodeEmail({ email, otp })
      },
    }),
  ],
  1. Create a custom login form with the following submit button handling:
const sendOtp = async (e: React.FormEvent) => {
    const result = await authClient.emailOtp.sendVerificationOtp({
        email,
        type: "sign-in",
    })
    if (result.error) {
        // THIS IS NOT TRIGGERED
        setError(result.error.message || "Failed to send the code")
        return
    }
    // ... other code ...
}
  1. Try requesting OTP for a non-existent user
  2. Backend fails but frontend has no way to get why

Current vs. Expected behavior

Here's my Reddit thread where it all started: https://www.reddit.com/r/better_auth/comments/1jpxe9l/is_it_possible_to_check_the_existence_of_a_user/

My use-case: I just want to prevent new users from registering with OTP and show them a proper descriptive error if they try to log in with a non-existent email. At first I thought the disableSignUp flag would solve this problem, but it doesn't return the correct error when trying to login either. So I decided to make my custom logic, but I couldn't get it right either.

Handling custom exceptions from sendVerificationOTP might be useful for other devs who also want to implement some custom logic before sending the codes.

What version of Better Auth are you using?

1.2.5

Provide environment information

- OS: Docker, Linux, Mac
- Browser: Chrome, Firefox, any...

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

Backend

Auth config (if applicable)

export const auth = betterAuth({
  database: prismaAdapter(prisma, { provider: "postgresql" }),
  plugins: [
    emailOTP({
      disableSignUp: true,
      sendVerificationOTP: async ({ email, otp }) => {
        const user = await getUserByEmail(email)
        if (!user) {
          // THIS WORKS BUT DOES NOT RETURN THE ERROR FROM SERVER
          throw new Error("User with this email does not exist")
        }
        await sendOTPCodeEmail({ email, otp })
      },
    })
  ],
})

--- client ---

export const authClient = createAuthClient({
  plugins: [emailOTPClient()],
})

Additional context

The exact problem is in this piece of code: d1c86b0b24/packages/better-auth/src/plugins/email-otp/index.ts (L157)

Simpliest solution would be to wrap await options.sendVerificationOTP() function call into a try-catch block and return a custom error message specified in the exception, allowing frontent to show it and fail gracefully.

Right now any exception raised from the sendVerificationOTP are ignored and only raised on the backend, so frontend get either a 500 error or status: success.

Originally created by @vas3k on GitHub (Apr 7, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Configure a better-auth instance to use OTP codes for sign-ins with a custom logic for checking that user exists and not banned on a server. ```typescript plugins: [ emailOTP({ disableSignUp: true, sendVerificationOTP: async ({ email, otp }) => { const user = await getUserByEmail(email) if (!user) { // THIS WORKS BUT DOES NOT RETURN THE ERROR FROM SERVER throw new Error("User with this email does not exist") } await sendOTPCodeEmail({ email, otp }) }, }), ], ``` 2. Create a custom login form with the following submit button handling: ```typescript const sendOtp = async (e: React.FormEvent) => { const result = await authClient.emailOtp.sendVerificationOtp({ email, type: "sign-in", }) if (result.error) { // THIS IS NOT TRIGGERED setError(result.error.message || "Failed to send the code") return } // ... other code ... } ``` 3. Try requesting OTP for a non-existent user 4. Backend fails but frontend has no way to get why ### Current vs. Expected behavior Here's my Reddit thread where it all started: https://www.reddit.com/r/better_auth/comments/1jpxe9l/is_it_possible_to_check_the_existence_of_a_user/ My use-case: I just want to prevent new users from registering with OTP and show them a proper descriptive error if they try to log in with a non-existent email. At first I thought the disableSignUp flag would solve this problem, but it doesn't return the correct error when trying to login either. So I decided to make my custom logic, but I couldn't get it right either. Handling custom exceptions from `sendVerificationOTP` might be useful for other devs who also want to implement some custom logic before sending the codes. ### What version of Better Auth are you using? 1.2.5 ### Provide environment information ```bash - OS: Docker, Linux, Mac - Browser: Chrome, Firefox, any... ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript export const auth = betterAuth({ database: prismaAdapter(prisma, { provider: "postgresql" }), plugins: [ emailOTP({ disableSignUp: true, sendVerificationOTP: async ({ email, otp }) => { const user = await getUserByEmail(email) if (!user) { // THIS WORKS BUT DOES NOT RETURN THE ERROR FROM SERVER throw new Error("User with this email does not exist") } await sendOTPCodeEmail({ email, otp }) }, }) ], }) --- client --- export const authClient = createAuthClient({ plugins: [emailOTPClient()], }) ``` ### Additional context The exact problem is in this piece of code: https://github.com/better-auth/better-auth/blob/d1c86b0b2450af1b90a409f54ffe15bbe882c1f9/packages/better-auth/src/plugins/email-otp/index.ts#L157 Simpliest solution would be to wrap `await options.sendVerificationOTP()` function call into a try-catch block and return a custom error message specified in the exception, allowing frontent to show it and fail gracefully. Right now any exception raised from the sendVerificationOTP are ignored and only raised on the backend, so frontend get either a 500 error or `status: success`.
Author
Owner

@Bekacru commented on GitHub (Apr 9, 2025):

To return error from the server, you can throw APIError instead. Which you can import from better-auth/api.

throw new APIError(403)
@Bekacru commented on GitHub (Apr 9, 2025): To return error from the server, you can throw `APIError` instead. Which you can import from `better-auth/api`. ```ts throw new APIError(403) ```
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#993