---
title: "Errors"
description: "Every code the package refuses with: the fields it carries, the English label, the status an auth route answers with, and what to do about it."
canonical_url: "https://nuxt-backend.dev/docs/reference/backend/errors"
---
# Errors

> Every code the package refuses with: the fields it carries, the English label, the status an auth route answers with, and what to do about it.

A refusal from a package function is a `ConvexError` whose data is `{ code, ...fields }`. Convex
sends that data to the client in production too, where any other error arrives as "Server Error".
An auth route refuses with the same code and fields in its error body, at the status below.
[`getBackendError(error)`](https://nuxt-backend.dev/docs/data/errors) reads both and returns `{ code, data, message }`.
`BACKEND_ERRORS` in `nuxt-backend/functions` holds the catalog, and
`test/unit/error-catalog.test.ts` keeps this page in step with it. `/e/<code>` links to the code's
entry below.

`message` is the sentence to show: the throw site's own sentence for the person when it wrote one
(`data.message`), else the code's label from the `errors` group. Some reasons have a label of their
own (`errors.<code>_<reason>`), and a label with numbers in it is used only when the numbers are
there. The English the deployment log shows is the error's `message` property, which production
never sends to the client.

Every field in `data` is optional on the read side, the required ones too: the reader keeps only
the fields that fit the catalog, so check the one you use before you rely on it.

## Codes

### `unauthenticated`

<table>
<thead>
  <tr>
    <th>
      Data
    </th>
    
    <th>
      Label
    </th>
    
    <th>
      Status
    </th>
    
    <th>
      Handling
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        message?
      </code>
    </td>
    
    <td>
      Sign in to continue.
    </td>
    
    <td>
      401
    </td>
    
    <td>
      Show the message. If nobody is signed in, send the person to sign in, then retry.
    </td>
  </tr>
</tbody>
</table>

Better Auth answers some of its own refusals with 401 too, and the reader maps them here with Better
Auth's message: a wrong password on a sign-in form, for one, where the person is already on the
sign-in page and only needs the message.

### `forbidden`

<table>
<thead>
  <tr>
    <th>
      Data
    </th>
    
    <th>
      Label
    </th>
    
    <th>
      Status
    </th>
    
    <th>
      Handling
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        reason
      </code>
      
      , <code>
        message?
      </code>
    </td>
    
    <td>
      You don't have access to this.
    </td>
    
    <td>
      403
    </td>
    
    <td>
      Show the message; <code>
        reason
      </code>
      
       says what would change the answer.
    </td>
  </tr>
</tbody>
</table>

### `no_workspace`

<table>
<thead>
  <tr>
    <th>
      Data
    </th>
    
    <th>
      Label
    </th>
    
    <th>
      Status
    </th>
    
    <th>
      Handling
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        message?
      </code>
    </td>
    
    <td>
      Create or choose a workspace first.
    </td>
    
    <td>
      409
    </td>
    
    <td>
      Offer the workspace switcher, or create one.
    </td>
  </tr>
</tbody>
</table>

### `rate_limited`

<table>
<thead>
  <tr>
    <th>
      Data
    </th>
    
    <th>
      Label
    </th>
    
    <th>
      Status
    </th>
    
    <th>
      Handling
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        retryAfter?
      </code>
      
       (ms), <code>
        limit?
      </code>
      
      , <code>
        message?
      </code>
    </td>
    
    <td>
      Too many requests. Try again in a moment.
    </td>
    
    <td>
      429
    </td>
    
    <td>
      Wait <code>
        retryAfter
      </code>
      
      , then retry. <code>
        limit
      </code>
      
       names the spent limit.
    </td>
  </tr>
</tbody>
</table>

### `insufficient_credits`

<table>
<thead>
  <tr>
    <th>
      Data
    </th>
    
    <th>
      Label
    </th>
    
    <th>
      Status
    </th>
    
    <th>
      Handling
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        meter?
      </code>
      
      , <code>
        balance
      </code>
      
      , <code>
        need
      </code>
      
      , <code>
        message?
      </code>
    </td>
    
    <td>
      Not enough credits. Top up to continue.
    </td>
    
    <td>
      402
    </td>
    
    <td>
      Offer a credit pack or a plan with more credits.
    </td>
  </tr>
</tbody>
</table>

With both amounts, the label reads "Not enough credits: {balance} left, {need} needed. Top up to
continue." (`errors.insufficient_credits_amounts`).

### `invalid_input`

<table>
<thead>
  <tr>
    <th>
      Data
    </th>
    
    <th>
      Label
    </th>
    
    <th>
      Status
    </th>
    
    <th>
      Handling
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        field
      </code>
      
      , <code>
        message?
      </code>
    </td>
    
    <td>
      That input isn't valid. Check it and try again.
    </td>
    
    <td>
      400
    </td>
    
    <td>
      Mark <code>
        field
      </code>
      
       and show the message.
    </td>
  </tr>
</tbody>
</table>

A checkout, a gift checkout, a plan change and the portal refuse what only the server may choose with this code: `productIds`, `productId` or `pack` outside the catalog (a plan change to a credit pack too), an `origin`, `successUrl` or `returnUrl` off the site, reserved `metadata` keys, a client's `trialInterval`, `trialIntervalCount` or `discountId`, a `trial` from server code that is not 1 to 30 days in days or weeks, and a `discountCode` the provider does not accept. See [What the server decides](https://nuxt-backend.dev/docs/billing/subscriptions/checkout#what-the-server-decides).

### `not_ready`

<table>
<thead>
  <tr>
    <th>
      Data
    </th>
    
    <th>
      Label
    </th>
    
    <th>
      Status
    </th>
    
    <th>
      Handling
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        reason
      </code>
      
      , <code>
        message?
      </code>
    </td>
    
    <td>
      Nothing to show yet.
    </td>
    
    <td>
      409
    </td>
    
    <td>
      Don't offer a retry: something has to happen first. <code>
        reason
      </code>
      
       says what (below).
    </td>
  </tr>
</tbody>
</table>

### `unavailable`

<table>
<thead>
  <tr>
    <th>
      Data
    </th>
    
    <th>
      Label
    </th>
    
    <th>
      Status
    </th>
    
    <th>
      Handling
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        service
      </code>
      
      , <code>
        reason
      </code>
      
      , <code>
        message?
      </code>
    </td>
    
    <td>
      This isn't available right now. Try again shortly.
    </td>
    
    <td>
      503
    </td>
    
    <td>
      Retry later only for <code>
        provider_refused
      </code>
      
      ; the other reasons need the deployment fixed, and <code>
        reason
      </code>
      
       tells an operator what.
    </td>
  </tr>
</tbody>
</table>

## Reasons

The `forbidden` reasons the package throws:

<table>
<thead>
  <tr>
    <th>
      Reason
    </th>
    
    <th>
      When
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        banned
      </code>
    </td>
    
    <td>
      The account is banned.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        role
      </code>
      
       · <code>
        permission
      </code>
    </td>
    
    <td>
      The account's role lacks the role or the permission statement the call requires.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        not_member
      </code>
      
       · <code>
        workspace_role
      </code>
    </td>
    
    <td>
      The caller is not a member of the workspace, or lacks the workspace role.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        admin_only
      </code>
    </td>
    
    <td>
      A billing admin function (<code>
        refundOrder
      </code>
      
      , the webhook log) and the caller is no admin.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        not_owner
      </code>
    </td>
    
    <td>
      The order, subscription or agent request belongs to another account, or the order does not exist: the two answer alike.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        impersonating
      </code>
    </td>
    
    <td>
      An administrator acting as this account tried something only its owner may do.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        sign_in_refused
      </code>
    </td>
    
    <td>
      <code>
        canSignIn
      </code>
      
       said no; its message is the one shown.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        customer_taken
      </code>
    </td>
    
    <td>
      With <code>
        billTo: 'user'
      </code>
      
      , the account's email already pays for another account (the provider keeps an individual customer's email unique); or the entity is mapped onto a customer from before 0.2 that another entity is keyed to.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        not_billing_manager
      </code>
    </td>
    
    <td>
      The billing portal of a workspace, opened by a member the provider does not list as its owner or a billing manager.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        gift_refused
      </code>
      
       · <code>
        not_recipient
      </code>
      
       · <code>
        no_email
      </code>
    </td>
    
    <td>
      A gift was refused (to the purchaser's own address, before their first paid order, or by <code>
        canGift
      </code>
      
      , whose message is the one shown), the gift is addressed to another email or does not exist, or the session has no verified email.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        gift_refunded
      </code>
    </td>
    
    <td>
      The gift's purchase was refunded before its claim, so there is nothing to receive.
    </td>
  </tr>
</tbody>
</table>

A `forbidden` the reader maps from Better Auth's own 403 carries Better Auth's error code, lower-cased,
as its `reason` (`you_are_not_allowed_to_ban_users`, say). Those reasons pass through as Better Auth
names them: they are not in the table above and not covered by the stability promise.

The `not_ready` reasons:

<table>
<thead>
  <tr>
    <th>
      Reason
    </th>
    
    <th>
      When
    </th>
    
    <th>
      Label
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        no_customer
      </code>
    </td>
    
    <td>
      The billing entity has no provider customer yet: the portal, a usage report or a refund needs one, and a first checkout creates it.
    </td>
    
    <td>
      Nothing to show yet — this opens after your first purchase.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        no_catalog
      </code>
    </td>
    
    <td>
      The app sells nothing yet: no catalog products, so a checkout, a gift or a plan change has nothing to sell. Declare plans and packs in <code>
        billing.catalog.ts
      </code>
      
       and run <code>
        npx nuxt-backend billing sync
      </code>
      
      .
    </td>
    
    <td>
      Nothing to buy yet — this app has no plans or packs set up.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        not_paid
      </code>
    </td>
    
    <td>
      A gift whose purchase is not paid yet can't be claimed.
    </td>
    
    <td>
      Nothing to show yet.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        not_synced
      </code>
    </td>
    
    <td>
      The credit balance never synced for this entity: no credits have reached it, or a mutation spent with <code>
        allowRefresh: false
      </code>
      
      . <code>
        syncEntitlements
      </code>
      
       or a credit pack changes it.
    </td>
    
    <td>
      Nothing to show yet — no credits have reached this account. Open billing to refresh, or get a credit pack.
    </td>
  </tr>
</tbody>
</table>

The `unavailable` reasons, by service:

<table>
<thead>
  <tr>
    <th>
      Service
    </th>
    
    <th>
      Reason
    </th>
    
    <th>
      When
    </th>
    
    <th>
      Retry?
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        billing
      </code>
    </td>
    
    <td>
      <code>
        not_configured
      </code>
    </td>
    
    <td>
      <code>
        BILLING_ACCESS_TOKEN
      </code>
      
       is not set on the deployment. Label: "This isn't set up on this app yet."
    </td>
    
    <td>
      No
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        billing
      </code>
    </td>
    
    <td>
      <code>
        provider_refused
      </code>
    </td>
    
    <td>
      The billing provider refused the request; the log has <code>
        billing.provider_failed
      </code>
      
      .
    </td>
    
    <td>
      Yes
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        email
      </code>
    </td>
    
    <td>
      <code>
        provider_refused
      </code>
    </td>
    
    <td>
      The email provider refused a call (a segment, a contact, a broadcast); its words are in the error's <code>
        message
      </code>
      
      , for the log.
    </td>
    
    <td>
      Yes
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        email
      </code>
    </td>
    
    <td>
      <code>
        test_mode
      </code>
      
       · <code>
        unconfigured
      </code>
    </td>
    
    <td>
      A code or an invitation would not arrive: the deployment is in test mode and the address is no test address, or it has no email key. The log has <code>
        email.undeliverable
      </code>
      
      . The message says the address can't receive mail here, or that the app can't send email yet.
    </td>
    
    <td>
      No
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        backend
      </code>
    </td>
    
    <td>
      <code>
        unexpected
      </code>
    </td>
    
    <td>
      Never thrown: a composable's <code>
        error
      </code>
      
       reads a failure that carries no code this way (a function that crashed, which production words as "Server Error", or an app's own <code>
        ConvexError
      </code>
      
      ). It says nothing about whether a service is down. Its <code>
        message
      </code>
      
       is the app's own sentence when it wrote one, else the label "Something went wrong." See <a href="https://nuxt-backend.dev/docs/reference/nuxt/conventions">
        Composable conventions
      </a>
      
      .
    </td>
    
    <td>
      Maybe
    </td>
  </tr>
</tbody>
</table>

A `rate_limited` from a package limit names it in `limit`: `emailOtp`, `emailOtpGlobal`,
`emailOtpAttempt`, `emailChange`, `billingSync`, `ai`, `aiBudget`, `admin`, `invitation`,
`invitationGlobal` and `agentRegistrationGlobal` (see `DEFAULT_LIMITS` in
`nuxt-backend/rate-limit`), and `discountCode`: the package's own count of discount codes the
provider refused at checkout, ten per billing entity in a fixed one-hour window, kept whether or
not the app passes a rate limiter. A code in flight counts until the provider answers, so parallel
checkouts can't try more between them. And `gifts`: the gifts one purchaser bought in the
`setupBilling({ giftLimit })` window, ten in thirty days by default. A limit of your own, enforced
with `throws: true`, reads the same way.

<page-keys>

- API reference: [/docs/reference/api/functions](https://nuxt-backend.dev/docs/reference/api/functions)
- Source: [https://github.com/qruto/nuxt-backend/blob/main/src/convex/errors.ts](https://github.com/qruto/nuxt-backend/blob/main/src/convex/errors.ts)

</page-keys>


## Sitemap

See the full [sitemap](https://nuxt-backend.dev/sitemap.md) for all pages.
