---
title: "Handle errors"
description: "Read a refusal's code with getBackendError, show the right sentence or the way out, and refuse from your own functions with the same codes."
canonical_url: "https://nuxt-backend.dev/docs/data/errors"
---
# Handle errors

> Read a refusal's code with getBackendError, show the right sentence or the way out, and refuse from your own functions with the same codes.

When a package function refuses a call (signed out, out of credits, rate limited), it throws a
`ConvexError` with a stable code: `{ code: 'insufficient_credits', balance: 0, need: 1 }`. Convex
sends that data to the client in production as well. A plain `Error` arrives there as "Server
Error", whatever its message said. The [Errors](https://nuxt-backend.dev/docs/reference/backend/errors) reference lists
every code and its fields.

## Read a refusal

`getBackendError(error)` is auto-imported. It returns `{ code, data, message }` for a refusal and
`null` for anything else, such as a crash or an error of your own:

```vue
<script setup lang="ts">
import { api } from '#backend/api'

const transform = useAction(api.ai.transform)
const notice = ref<string | null>(null)
const outOfCredits = ref(false)

async function run(text: string) {
  notice.value = null
  try {
    await transform({ text })
  }
  catch (error) {
    const refusal = getBackendError(error)
    outOfCredits.value = refusal?.code === 'insufficient_credits'
    notice.value = refusal?.message ?? 'Something went wrong. Try again.'
  }
}
</script>
```

`message` is the sentence to show: the throw site's own sentence for the person when it wrote one
(an app's `canSignIn` refusal, say), else the code's English label. The data is typed by code, so
after `refusal.code === 'rate_limited'` you can read `refusal.data.retryAfter`, the wait in
milliseconds. Every field may be absent on this side, so check the one you use: the reader keeps
only what fits the catalog.

It reads the same codes from every source:

- a package function's refusal, and one you throw with `backendError`;
- the rate limiter's own error, from any limit called with `throws: true`, as `rate_limited` with
the limit's name;
- an auth client result (`{ data, error }`) or its `error`: the package's auth refusals carry the
same codes, and Better Auth's own 401, 403 and 429 map to `unauthenticated`, `forbidden` and
`rate_limited`;
- any of these as an `Error`'s `cause`, which is how `unwrapAuth()` and the rejections of
`useOrganization()` pass them on.

`useAiStream()` keeps the refusal of its last `start` in `error`, and its `status` ends in
`'error'`.

## Branch on the code

<table>
<thead>
  <tr>
    <th>
      Code
    </th>
    
    <th>
      What to offer
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        unauthenticated
      </code>
    </td>
    
    <td>
      Sign in, then retry. On a sign-in form it can also be refused credentials: show the message.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        no_workspace
      </code>
    </td>
    
    <td>
      Choose or create a workspace.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        insufficient_credits
      </code>
    </td>
    
    <td>
      A credit pack or a bigger plan. <code>
        data.need
      </code>
      
       and <code>
        data.balance
      </code>
      
      , when present, say how far off it is.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        rate_limited
      </code>
    </td>
    
    <td>
      A countdown from <code>
        data.retryAfter
      </code>
      
      , when present.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        forbidden
      </code>
    </td>
    
    <td>
      The message. <code>
        data.reason
      </code>
      
       says why, <code>
        banned
      </code>
      
       or <code>
        role
      </code>
      
       for example.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        not_ready
      </code>
    </td>
    
    <td>
      No retry: the first purchase (<code>
        no_customer
      </code>
      
      ) or a credit pack (<code>
        not_synced
      </code>
      
      ) is what changes it.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        unavailable
      </code>
    </td>
    
    <td>
      A retry later for <code>
        provider_refused
      </code>
      
      . The other reasons need the deployment fixed; the log says what.
    </td>
  </tr>
</tbody>
</table>

## Refuse from your own functions

`backendError(code, data?, detail?)` from `nuxt-backend/functions` builds the error. The data is
checked against the code's fields, and `detail` is the English the deployment log shows:

```ts [backend/projects.ts]
import { backendError } from 'nuxt-backend/functions'
import { v } from 'convex/values'
import { authed } from './functions'

export const rename = authed.mutation({
  args: { id: v.id('projects'), name: v.string() },
  handler: async (ctx, { id, name }) => {
    const project = await ctx.db.get(id)
    if (project?.ownerId !== ctx.user.id) throw backendError('forbidden', { reason: 'not_owner' })
    if (!name.trim()) throw backendError('invalid_input', { field: 'name', message: 'Give the project a name.' })
    await ctx.db.patch(id, { name: name.trim() })
  },
})
```

The `authed`, `org` and `admin` builders from `createFunctions` already throw `unauthenticated`,
`no_workspace` and `forbidden`. A limit of your own refuses as `rate_limited` when you call it with
`throws: true`, but the limiter throws only when it knows the wait. Keep the result, and refuse
yourself when it says no:

```ts
const { ok, retryAfter } = await rateLimiter.limit(ctx, 'sendMessage', { key: ctx.user.id, throws: true })
if (!ok) throw backendError('rate_limited', { limit: 'sendMessage', retryAfter: retryAfter || undefined })
```

In an auth hook, throw the auth library's `APIError` with the code and its fields in the body, and
the client reads it the same way:

```ts
import { APIError } from 'nuxt-backend/auth'

throw new APIError('BAD_REQUEST', { code: 'invalid_input', field: 'email', message: 'Invite a company address.' })
```

## Related

- [Errors](https://nuxt-backend.dev/docs/reference/backend/errors): every code, its fields, label and status.
- [Function builders](https://nuxt-backend.dev/docs/auth/access/functions): the tiers that refuse for you.
- [Rate limits](https://nuxt-backend.dev/docs/ship/production/rate-limits): the package's limits and what a person sees.
- [Metered actions](https://nuxt-backend.dev/docs/ai/metered-actions): the refusals before a spend.

<page-keys>

- Try it live in the playground: [/playground/platform/ai](https://nuxt-backend.dev/playground/platform/ai)
- 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.
