[GH-ISSUE #8834] sendOTP errors in phone-number plugin are silently swallowed, endpoint always returns 200 #19839

Closed
opened 2026-04-15 19:11:33 -05:00 by GiteaMirror · 1 comment
Owner

Originally created by @hammer-ai on GitHub (Mar 30, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/8834

Originally assigned to: @ping-maxwell on GitHub.

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

When using the phoneNumber plugin, if the sendOTP callback throws an error (e.g. SMS provider returns an auth failure), the /phone-number/send-otp endpoint still returns 200 { message: "code sent" }. The client has no way to know the OTP was never delivered. To reproduce:

  1. Configure the phoneNumber plugin with a sendOTP that throws:
  phoneNumber({
    sendOTP: async ({ phoneNumber, code }) => {
      throw new Error('SMS provider error')
    }
  })
  1. Do not configure advanced.backgroundTasks.handler
  2. Call POST /api/auth/phone-number/send-otp with a phone number
  3. Observe: endpoint returns 200 { message: "code sent" } and the error is only logged server-side

This happens because runInBackgroundOrAwait catches all errors without rethrowing:

  // create-context.mjs
  async runInBackgroundOrAwait(promise) {
      try {
          if (options.advanced?.backgroundTasks?.handler) {
              if (promise instanceof Promise)
  options.advanced.backgroundTasks.handler(promise.catch((e) => {
                  logger.error("Failed to run background task:", e);
              }));
          } else await promise;
      } catch (e) {
          logger.error("Failed to run background task:", e);
      }
  },

Both code paths swallow the error:

  • With backgroundTasks.handler: Fire-and-forget (expected, since it's a background task)
  • Without backgroundTasks.handler: The promise is awaited, but the catch block logs the error and does not rethrow — so the endpoint continues to return ctx.json({ message: "code sent" })

The second path should rethrow (or the endpoint should check the result) so that callers who are not using background tasks can surface SMS delivery failures to the client.

Current vs. Expected behavior

When backgroundTasks.handler is not configured and sendOTP throws, the /phone-number/send-otp endpoint should return an error response (e.g. 500) so the client can inform the user.

What version of Better Auth are you using?

1.5.6

System info

N/A

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

Client

Auth config (if applicable)

import { betterAuth } from "better-auth"
export const auth = betterAuth({
  emailAndPassword: {  
    enabled: true
  },
});

Additional context

No response

Originally created by @hammer-ai on GitHub (Mar 30, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/8834 Originally assigned to: @ping-maxwell on GitHub. ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce When using the phoneNumber plugin, if the `sendOTP` callback throws an error (e.g. SMS provider returns an auth failure), the `/phone-number/send-otp` endpoint still returns `200 { message: "code sent" }`. The client has no way to know the OTP was never delivered. To reproduce: 1. Configure the phoneNumber plugin with a sendOTP that throws: ``` phoneNumber({ sendOTP: async ({ phoneNumber, code }) => { throw new Error('SMS provider error') } }) ``` 2. Do not configure `advanced.backgroundTasks.handler` 3. Call POST `/api/auth/phone-number/send-otp` with a phone number 4. Observe: endpoint returns `200 { message: "code sent" }` and the error is only logged server-side This happens because runInBackgroundOrAwait catches all errors without rethrowing: ``` // create-context.mjs async runInBackgroundOrAwait(promise) { try { if (options.advanced?.backgroundTasks?.handler) { if (promise instanceof Promise) options.advanced.backgroundTasks.handler(promise.catch((e) => { logger.error("Failed to run background task:", e); })); } else await promise; } catch (e) { logger.error("Failed to run background task:", e); } }, ``` Both code paths swallow the error: - With `backgroundTasks.handler`: Fire-and-forget (expected, since it's a background task) - Without `backgroundTasks.handler`: The promise is awaited, but the catch block logs the error and does not rethrow — so the endpoint continues to return ctx.json({ message: "code sent" }) The second path should rethrow (or the endpoint should check the result) so that callers who are not using background tasks can surface SMS delivery failures to the client. ### Current vs. Expected behavior When backgroundTasks.handler is not configured and sendOTP throws, the `/phone-number/send-otp` endpoint should return an error response (e.g. 500) so the client can inform the user. ### What version of Better Auth are you using? 1.5.6 ### System info ```bash N/A ``` ### Which area(s) are affected? (Select all that apply) Client ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth" export const auth = betterAuth({ emailAndPassword: { enabled: true }, }); ``` ### Additional context _No response_
GiteaMirror added the bug label 2026-04-15 19:11:33 -05:00
Author
Owner

@GautamBytes commented on GitHub (Mar 30, 2026):

Looking into it!

<!-- gh-comment-id:4154214884 --> @GautamBytes commented on GitHub (Mar 30, 2026): Looking into it!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#19839