moeru-ai/airi · error · Error

listAccounts failed

Error message

listAccounts failed

What it means

Thrown inside refresh() when the better-auth typed client's listAccounts() resolves with a non-null `error` object. The composable only falls back to the generic 'listAccounts failed' string when the server error carried no message. It surfaces backend auth-service failures to the UI error ref rather than silently showing an empty list.

Source

Thrown at packages/stage-ui/src/composables/use-linked-accounts.ts:162

  )

  /**
   * Client-side mirror of better-auth's `FAILED_TO_UNLINK_LAST_ACCOUNT`
   * guard so we can surface a user-friendly message before round-tripping.
   */
  function isLastSignInMethod(providerId: string): boolean {
    if (providerId === 'credential')
      return socialLinkedCount.value === 0
    return !hasCredentialAccount.value && socialLinkedCount.value <= 1
  }

  async function refresh() {
    loading.value = true
    error.value = null
    try {
      const { data, error: apiError } = await args.client.listAccounts()
      if (apiError)
        throw new Error(apiError.message ?? 'listAccounts failed')
      // better-auth 1.6.6 widens listAccounts elements to `any`; consume
      // the row directly rather than dressing `any` up with a fake shape.
      // Field layout: node_modules/better-auth/dist/api/routes/account.mjs L20-50.
      linkedAccounts.value = (data ?? []).map(account => ({
        id: account.id,
        accountId: account.accountId,
        providerId: account.providerId,
        createdAt: account.createdAt instanceof Date
          ? account.createdAt.toISOString()
          : account.createdAt,
        scopes: account.scopes ?? [],
      }))
      loaded.value = true
    }
    catch (err) {
      // Keep prior `linkedAccounts` on error so a transient 5xx doesn't
      // flip `hasCredentialAccount` and mis-route the password UI.
      // Source: PR #1753 review (chatgpt-codex-connector P2).

View on GitHub (pinned to 27111382b4)

Solutions

  1. Check the auth service health and the network tab: the real status/message is in apiError.message — inspect `error.value` in the composable.
  2. If the session expired, re-authenticate so the credentialed/Bearer client regains a valid session, then call refresh() again.
  3. Verify better-auth base URL and CORS configuration allow the requesting origin.
  4. Treat transient 5xx as retryable: the composable intentionally keeps prior linkedAccounts on error, so a manual refresh() after the outage recovers.
Defensive patterns

Strategy: try-catch

Validate before calling

// refresh() already clears error and sets loading; pre-check auth before mount
if (!args.isAuthenticated.value) {
  // skip refresh; listAccounts requires an active session
}

Type guard

null

Try / catch

// already implemented in refresh(): error surfaced via error.value, prior list kept
try {
  const { data, error: apiError } = await args.client.listAccounts()
  if (apiError) throw new Error(apiError.message ?? 'listAccounts failed')
  // ...
}
catch (err) {
  error.value = args.describeError(err) || args.messages.listFailed
}

Prevention

When it happens

Trigger: args.client.listAccounts() returns { data: null, error: { ... } }. This happens when the auth backend returns non-2xx (5xx outage, 401 expired session cookie, 403 forbidden), the network request fails and better-auth wraps it, or the auth service URL is misconfigured/unreachable.

Common situations: Auth service is down or restarting. The user's session cookie expired and silent refresh also failed (stage-web Bearer token revoked). The better-auth base URL env var points at the wrong host. CORS blocks the request in stage-web.

Related errors


AI-assisted analysis of moeru-ai/airi@27111382b4 (2026-08-12). Data as JSON: /api/errors/9640cb85aaf259eb. Report an issue: GitHub.