Skip to main content

Error handling

Handle query, mutation, action, auth, upload, and server failures with one public error model.

Handle errors at the boundary that can make the next decision.

Choose the control flow

OperationNormal pattern
QueryRender reactive error and offer refresh() where safe
Mutation/actionCall mutate or run, catch, and map structured domain errors
FormRender fieldErrors and formError from the submit result
PaginationShow a first-page error by status; retry a later page with loadMore()
Auth operationInspect Better Auth operation result plus reactive auth error
UploadRender upload error; retry with a new explicit user action
Server routeRethrow with toConvexH3Error, or map to your own HTTP contract

Query retry

vue
<section v-if="error">
  <p>Projects are unavailable.</p>
  <button @click="refresh">Try again</button>
</section>

Do not retry a required query while auth is anonymous. Let auth state change first.

Structured server errors

ts
import { isConvexCallError } from '@lupinum/better-convex-nuxt/errors'

const { mutate: archiveProject } = useConvexMutation(api.projects.archive)

async function archive() {
  try {
    await archiveProject({ projectId })
  } catch (error) {
    if (isConvexCallError(error, 'PROJECT_ALREADY_ARCHIVED')) {
      await navigateTo('/archive')
      return
    }
    toast.error(isConvexCallError(error) ? error.message : 'The project could not be archived.')
  }
}

Use code, not message substrings, for decisions. A server error's code is the data.code of the ConvexError, and its message is the text the Convex function wrote in data.message. Show that message to the user only when the backend writes user-facing text.

Library error codes

The codes that Better Convex raises itself include IDENTITY_CHANGED, CANCELLED, FILE_TOO_LARGE, FILE_TYPE_NOT_ALLOWED, UPLOAD_IN_PROGRESS, SUBMIT_IN_PROGRESS, UNAUTHENTICATED, CLIENT_UNAVAILABLE, NETWORK_ERROR, and TIMEOUT. The type ConvexCallErrorCode lists every code. Ignore CANCELLED when your own cancel() or reset() caused it. Do not retry IDENTITY_CHANGED automatically. See error types.

Errors from Nitro routes

A route that rethrows with toConvexH3Error returns the serialized error. Revive it in the browser:

ts
import { normalizeConvexError } from '@lupinum/better-convex-nuxt/errors'

try {
  await $fetch('/api/reports', { method: 'POST', body })
} catch (raw) {
  const error = normalizeConvexError(raw)
  reportError.value = error.message
}

normalizeConvexError reads the error from the FetchError of $fetch or useFetch. Any other value becomes an unknown error with a generic message.

Error boundaries

Vue/Nuxt error boundaries are appropriate for render failures or page-level inability to continue. A failed button action usually belongs inline and should not replace the whole page.

Logging

Log the category, functionName, and safe correlation context. Do not log tokens, cookies, auth headers, raw upload bytes, or upstream error objects. ConvexCallError does not retain the raw upstream cause.

Retry safety

Queries are safe to repeat. Mutations and actions may have committed before a network or identity-transition failure became visible. Add backend idempotency where retries are possible.