[GH-ISSUE #3443] BetterAuth Client Promises Do Not Reject on Error, Breaking Compatibility with Promise-Based Libraries #9603

Closed
opened 2026-04-13 05:09:16 -05:00 by GiteaMirror · 3 comments
Owner

Originally created by @gottfrois on GitHub (Jul 18, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/3443

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

I've recently encountered an issue with the promise returned by authClient.signIn.magicLink, which does not reject when an API error occurs (e.g., an HTTP 400 Bad Request). Instead, it relies on onSuccess and onError callbacks within the fetchOptions to handle the outcome of the request.

While this report focuses specifically on the magicLink function, this behavior may be a more general pattern across other BetterAuth client functions. If so, it deviates from the standard JavaScript promise pattern, where an asynchronous operation is expected to reject its promise upon failure. This makes it difficult to integrate with modern libraries like sonner, which relies on a promise's resolution or rejection to manage UI states (e.g., loading, success, error) via functions like toast.promise.

Current vs. Expected behavior

When using authClient.signIn.magicLink, an error does not cause the returned promise to reject, preventing promise-chaining error handling (like .catch()) and breaking tools that depend on it.

Example with sonner:

// This promise will not reject on a 400 error.
const promise = authClient.signIn.magicLink({
  email: "invalid-email", // Intentionally cause an error
  fetchOptions: {
    // The error is handled here, but the promise itself never rejects.
    onError: (error) => {
      console.error("Request failed:", error);
    },
  },
});

// Because the promise doesn't reject, the `error` state is never triggered.
toast.promise(promise, {
  loading: "Sending magic link...",
  success: "Link sent successfully!",
  error: "Error: This toast is never displayed.", // This is the issue
});

Suggested Solution & Workaround:

The ideal solution would be for promises returned by BetterAuth client functions to reject when the underlying fetch request fails. This would align with modern asynchronous patterns and improve developer experience by allowing standard async/await with try/catch or .catch() blocks.

In the meantime, the only way to make this work with promise-aware libraries is to wrap the call in a new Promise and manually trigger resolve and reject:

Workaround Code:

const promise = new Promise((resolve, reject) =>
  authClient.signIn.magicLink({
    email: form.email,
    fetchOptions: {
      onSuccess: (response) => resolve(response),
      onError: (error) => reject(error),
    },
  }),
);

// This now works as expected.
toast.promise(promise, {
  loading: "Sending magic link...",
  success: "Link sent successfully!",
  error: "Failed to send the link.",
});

What version of Better Auth are you using?

1.2.12

Provide environment information

- OS: MacOS
- Browser: Brave

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

Client

Auth config (if applicable)

import { betterAuth } from "better-auth"
import { magicLink } from "better-auth/plugins";

export const auth = betterAuth({
  emailAndPassword: {  
    enabled: false
  },
  plugins: [
    magicLink({ ... })
  ]
});

Additional context

No response

Originally created by @gottfrois on GitHub (Jul 18, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/3443 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce I've recently encountered an issue with the promise returned by `authClient.signIn.magicLink`, which does not reject when an API error occurs (e.g., an HTTP 400 Bad Request). Instead, it relies on `onSuccess` and `onError` callbacks within the `fetchOptions` to handle the outcome of the request. While this report focuses specifically on the `magicLink` function, this behavior may be a more general pattern across other BetterAuth client functions. If so, it deviates from the standard JavaScript promise pattern, where an asynchronous operation is expected to `reject` its promise upon failure. This makes it difficult to integrate with modern libraries like `sonner`, which relies on a promise's resolution or rejection to manage UI states (e.g., loading, success, error) via functions like `toast.promise`. ### Current vs. Expected behavior When using `authClient.signIn.magicLink`, an error does not cause the returned promise to reject, preventing promise-chaining error handling (like `.catch()`) and breaking tools that depend on it. **Example with `sonner`:** ```javascript // This promise will not reject on a 400 error. const promise = authClient.signIn.magicLink({ email: "invalid-email", // Intentionally cause an error fetchOptions: { // The error is handled here, but the promise itself never rejects. onError: (error) => { console.error("Request failed:", error); }, }, }); // Because the promise doesn't reject, the `error` state is never triggered. toast.promise(promise, { loading: "Sending magic link...", success: "Link sent successfully!", error: "Error: This toast is never displayed.", // This is the issue }); ``` **Suggested Solution & Workaround:** The ideal solution would be for promises returned by BetterAuth client functions to **reject** when the underlying fetch request fails. This would align with modern asynchronous patterns and improve developer experience by allowing standard `async/await` with `try/catch` or `.catch()` blocks. In the meantime, the only way to make this work with promise-aware libraries is to wrap the call in a new `Promise` and manually trigger `resolve` and `reject`: **Workaround Code:** ```javascript const promise = new Promise((resolve, reject) => authClient.signIn.magicLink({ email: form.email, fetchOptions: { onSuccess: (response) => resolve(response), onError: (error) => reject(error), }, }), ); // This now works as expected. toast.promise(promise, { loading: "Sending magic link...", success: "Link sent successfully!", error: "Failed to send the link.", }); ``` ### What version of Better Auth are you using? 1.2.12 ### Provide environment information ```bash - OS: MacOS - Browser: Brave ``` ### Which area(s) are affected? (Select all that apply) Client ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth" import { magicLink } from "better-auth/plugins"; export const auth = betterAuth({ emailAndPassword: { enabled: false }, plugins: [ magicLink({ ... }) ] }); ``` ### Additional context _No response_
GiteaMirror added the lockedbug labels 2026-04-13 05:09:16 -05:00
Author
Owner

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

You're correct—BetterAuth client functions (including signIn.magicLink) do not reject their promises on API errors. Instead, they use onSuccess and onError callbacks in fetchOptions for error handling. This pattern is consistent across client methods and not limited to magicLink, which means standard promise-based error handling (like .catch() or async/await try/catch) and promise-aware UI libraries (such as toast.promise in sonner) won't work out of the box. There isn't any documentation or open issue explaining this design or suggesting alternatives.

The current workaround is to wrap the call in a new Promise and manually resolve or reject based on the callbacks, as you've shown. There are no signs of this changing in the codebase or roadmap right now, so this manual approach is the best option for compatibility with promise-based libraries.
See code details

To reply, just mention @dosu.


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

<!-- gh-comment-id:3088859138 --> @dosubot[bot] commented on GitHub (Jul 18, 2025): <!-- Answer --> You're correct—BetterAuth client functions (including signIn.magicLink) do not reject their promises on API errors. Instead, they use onSuccess and onError callbacks in fetchOptions for error handling. This pattern is consistent across client methods and not limited to magicLink, which means standard promise-based error handling (like .catch() or async/await try/catch) and promise-aware UI libraries (such as toast.promise in sonner) won't work out of the box. There isn't any documentation or open issue explaining this design or suggesting alternatives. The current workaround is to wrap the call in a new Promise and manually resolve or reject based on the callbacks, as you've shown. There are no signs of this changing in the codebase or roadmap right now, so this manual approach is the best option for compatibility with promise-based libraries. [See code details](https://github.com/better-auth/better-auth/blob/4c9479ee2bab2f6515f713a145418a9b1141cbe9/packages/better-auth/src/client/proxy.ts) <!-- 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/ce624d47-8826-4b40-b528-dc22b6db5e95?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/ce624d47-8826-4b40-b528-dc22b6db5e95?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/ce624d47-8826-4b40-b528-dc22b6db5e95?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/ce624d47-8826-4b40-b528-dc22b6db5e95?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/ce624d47-8826-4b40-b528-dc22b6db5e95?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/ce624d47-8826-4b40-b528-dc22b6db5e95?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/ce624d47-8826-4b40-b528-dc22b6db5e95?feedback_type=other)</sup>&nbsp;&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/3443)
Author
Owner

@Kinfe123 commented on GitHub (Jul 18, 2025):

BetterAuth intentionally uses a response object pattern ({ data, error }) instead of throwing errors. it uses error as value. it doesn't follow the promise rejection pattern that libraries like sonner expect. Here's how to do these patterns like by wrapper function withPromiseRejection that converts BA's callback-style error handling into proper promise rejections. something like this -

function withPromiseRejection<T>(authCall: (options: any) => Promise<any>) {
  return (options: any) => {
    return new Promise((resolve, reject) => {
      authCall({
        ...options,
        fetchOptions: {
          ...options?.fetchOptions,
          onError: (error: any) => {
            options?.fetchOptions?.onError?.(error);
            reject(error);
          },
          onSuccess: (response: any) => {
            options?.fetchOptions?.onSuccess?.(response);
            resolve(response.data);
          },
        },
      }).catch(reject);
    });
  };
}

const sendMagicLink = withPromiseRejection(authClient.signIn.magicLink);

toast.promise(sendMagicLink({ email: "user@example.com" }), {
  loading: "Sending magic link...",
  success: "Link sent successfully!",
  error: "Failed to send the link.",
});

this is more of pseudocode. make to modify it to your need.

<!-- gh-comment-id:3089592119 --> @Kinfe123 commented on GitHub (Jul 18, 2025): BetterAuth intentionally uses a response object pattern ({ data, error }) instead of throwing errors. it uses error as value. it doesn't follow the promise rejection pattern that libraries like sonner expect. Here's how to do these patterns like by wrapper function withPromiseRejection that converts BA's callback-style error handling into proper promise rejections. something like this - ```ts function withPromiseRejection<T>(authCall: (options: any) => Promise<any>) { return (options: any) => { return new Promise((resolve, reject) => { authCall({ ...options, fetchOptions: { ...options?.fetchOptions, onError: (error: any) => { options?.fetchOptions?.onError?.(error); reject(error); }, onSuccess: (response: any) => { options?.fetchOptions?.onSuccess?.(response); resolve(response.data); }, }, }).catch(reject); }); }; } const sendMagicLink = withPromiseRejection(authClient.signIn.magicLink); toast.promise(sendMagicLink({ email: "user@example.com" }), { loading: "Sending magic link...", success: "Link sent successfully!", error: "Failed to send the link.", }); ``` this is more of pseudocode. make to modify it to your need.
Author
Owner

@gottfrois commented on GitHub (Jul 23, 2025):

Thanks for sharing this, appreciate it.

<!-- gh-comment-id:3106924566 --> @gottfrois commented on GitHub (Jul 23, 2025): Thanks for sharing this, appreciate 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#9603