Guide

Error Handling

Handle server errors with ActionError and return field-level Zod validation errors to Vue components using nuxt-safe-action.

Error Handling

nuxt-safe-action provides two mechanisms for error handling: server errors (thrown exceptions) and validation errors (per-field error messages).

Server Errors

ActionError

Throw an ActionError to send a controlled error message to the client:

server/actions/purchase.ts
import { ActionError } from '#safe-action'

export default actionClient
  .schema(z.object({ itemId: z.string() }))
  .action(async ({ parsedInput, ctx }) => {
    const credits = await getCredits(ctx.userId)
    if (credits < 1) {
      throw new ActionError('Not enough credits')
    }
    // ...
  })

On the client, this error appears in serverError:

<script setup lang="ts">
const { execute, serverError } = useAction(purchase)
</script>

<template>
  <p v-if="serverError" class="error">{{ serverError }}</p>
</template>

handleServerError

The handleServerError callback in createSafeActionClient transforms all server errors before sending them to the client:

server/utils/action-client.ts
export const actionClient = createSafeActionClient({
  handleServerError: (error) => {
    // Log to your error tracking service
    console.error('Action error:', error.message)

    // Only expose ActionError messages to the client
    if (error instanceof ActionError) {
      return error.message
    }

    // Generic message for unexpected errors
    return 'Something went wrong'
  },
})

Unhandled Errors

Any error thrown in middleware or the action handler that isn't an ActionError is passed through handleServerError. This prevents leaking internal error details to the client.

Validation Errors

Zod Schema Errors

When input fails Zod validation, errors are automatically returned as validationErrors — an object mapping field names to error message arrays:

<script setup lang="ts">
const { execute, validationErrors } = useAction(createPost)
// validationErrors might be: { title: ['Title is required'], body: ['Body is required'] }
</script>

<template>
  <form @submit.prevent="execute({ title: '', body: '' })">
    <input v-model="title" />
    <span v-if="validationErrors?.title">{{ validationErrors.title[0] }}</span>

    <textarea v-model="body" />
    <span v-if="validationErrors?.body">{{ validationErrors.body[0] }}</span>
  </form>
</template>

Manual Validation Errors

Use returnValidationErrors to return field-level errors from inside your action handler:

server/actions/register.ts
import { returnValidationErrors } from '#safe-action'

export default actionClient
  .schema(z.object({
    email: z.string().email(),
    password: z.string().min(8),
  }))
  .action(async ({ parsedInput }) => {
    const existing = await db.user.findUnique({ where: { email: parsedInput.email } })
    if (existing) {
      returnValidationErrors({
        email: ['This email is already taken'],
      })
    }
    // Create user...
  })

These errors appear in the same validationErrors ref as Zod validation errors, so your form error display logic works the same way.

Error Flow Summary

Action called
  ├── Input validation fails → validationErrors returned
  ├── Middleware throws → handleServerError → serverError
  ├── Handler throws ActionError → handleServerError → serverError
  ├── Handler calls returnValidationErrors → validationErrors returned
  └── Handler throws unexpected error → handleServerError → serverError (sanitized)
Copyright © 2026