---
title: "Error Handling"
description: "Read query errors through status and error, recognize ConvexError data with isConvexError, handle failed mutations, and map Convex errors in server routes."
canonical_url: "https://nuxt-convex.onmax.me/nuxt-module/error-handling"
---
# Error Handling

> Read query errors through status and error, recognize ConvexError data with isConvexError, handle failed mutations, and map Convex errors in server routes.

Convex functions fail in two ways: a `ConvexError` you throw on purpose, carrying data for the client, and everything else. `nuxt-convex` keeps that distinction on the server, in the payload, and in the browser.

## Throw errors the client can read

Throw a `ConvexError` with a code, and any data the UI needs:

```ts [convex/projects.ts]
import { ConvexError, v } from 'convex/values'
import { query } from './_generated/server'

export const get = query({
  args: { id: v.id('projects') },
  handler: async (ctx, { id }) => {
    const project = await ctx.db.get(id)
    if (!project)
      throw new ConvexError({ code: 'NOT_FOUND', id })
    return project
  },
})
```

Other errors (a bug, a failed validator) reach the client as a plain `Error`. In production deployments Convex replaces their message with a generic one, so don't build UI on it.

## Queries: `status` and `error`

Queries never throw into your component. Awaiting one resolves even when it fails; read `status` and `error` instead:

```vue [app/pages/projects/[id].vue]
<script setup lang="ts">
import type { Id } from '~~/convex/_generated/dataModel'
import { api } from '#convex/api'
import { isConvexError } from '#convex'

const route = useRoute()
const { data: project, error, status } = await useConvexQuery(api.projects.get, () => ({
  id: route.params.id as Id<'projects'>,
}))

const notFound = computed(() => isConvexError(error.value, 'NOT_FOUND'))
</script>

<template>
  <p v-if="notFound">
    This project doesn't exist.
  </p>
  <p v-else-if="status === 'error'">
    Something went wrong.
  </p>
  <ProjectView v-else-if="project" :project="project" />
</template>
```

`status` is `'pending'`, `'success'`, `'error'`, or `'idle'` while the query is skipped. `error` is `null` unless `status` is `'error'`, and `data` is `undefined` while it is.

A query that fails during SSR is hydrated as an error, `ConvexError` data included, so the browser renders the same state without refetching. The WebSocket subscription then keeps running: if the underlying data changes so the query succeeds, `status` goes back to `'success'`.

### Turn a missing record into a 404 page

To let Nuxt render its error page with the right status, throw from the page yourself:

```ts
const { data: project, error } = await useConvexQuery(api.projects.get, { id })
if (isConvexError(error.value, 'NOT_FOUND'))
  throw createError({ statusCode: 404, statusMessage: 'Project not found', fatal: true })
```

### `suspense()` when you want a rejection

`suspense()` resolves with the first result and rejects with the error, for code that prefers `try`/`catch`:

```ts
const projects = useConvexQuery(api.projects.list)
try {
  const list = await projects.suspense()
}
catch (error) {
  // the same error as projects.error.value
}
```

## `isConvexError`

```ts
isConvexError(error: unknown, code?: string): error is ConvexError<Value>
```

Returns `true` for a `ConvexError`, including one rebuilt from the SSR payload. With `code`, it also checks `error.data === code` or `error.data.code === code`. It's exported from `#convex` (and `vue-convex`). See the [reference](https://nuxt-convex.onmax.me/api-reference/is-convex-error).

## Mutations and actions

`mutate()` and `execute()` reject when the function fails, and set the composable's `error` until the next call:

```vue [app/components/RenameProject.vue]
<script setup lang="ts">
import type { Id } from '~~/convex/_generated/dataModel'
import { api } from '#convex/api'
import { isConvexError } from '#convex'

const props = defineProps<{ id: Id<'projects'> }>()
const { mutate: rename, isPending, error } = useConvexMutation(api.projects.rename)
const name = ref('')

async function submit() {
  try {
    await rename({ id: props.id, name: name.value })
  }
  catch (cause) {
    // Shown below through `error`; anything else is unexpected
    if (!isConvexError(cause, 'NAME_TAKEN'))
      throw cause
  }
}
</script>

<template>
  <form @submit.prevent="submit">
    <input v-model="name">
    <button :disabled="isPending">
      Rename
    </button>
    <p v-if="isConvexError(error, 'NAME_TAKEN')">
      That name is taken.
    </p>
  </form>
</template>
```

They also reject with `Convex realtime client is not connected` when called during SSR, or before the app has a Convex URL.

## Server routes

The [server helpers](https://nuxt-convex.onmax.me/nuxt-module/server-routes) throw H3 errors, so a failed Convex call becomes a proper HTTP response:

- a `ConvexError` becomes a 400, or `data.status` when it's a number from 400 to 599, with the error's `data`
- anything else becomes a 500 whose message doesn't expose Convex internals

```ts [convex/projects.ts]
throw new ConvexError({ status: 403, code: 'FORBIDDEN', message: 'Not your project' })
```

A server route that calls a function throwing this error responds with a 403 and the message `Not your project`, and the error's `data` travels in the response body, so `useFetch` and `$fetch` callers can read the code.

Use `toConvexH3Error` yourself when you call a raw `ConvexHttpClient` from `useConvexServer(event).getClient()`.

## Related pages

- [`useConvexQuery`](https://nuxt-convex.onmax.me/api-reference/use-convex-query)
- [Server Routes](https://nuxt-convex.onmax.me/nuxt-module/server-routes)
- [Troubleshooting](https://nuxt-convex.onmax.me/troubleshooting)


## Sitemap

See the full [sitemap](https://nuxt-convex.onmax.me/sitemap.md) for all pages.
