Error Handling
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:
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:
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:
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)
Middleware
Chain composable middleware for authentication, authorization, logging, and rate limiting with fully typed context and H3Event access.
createSafeActionClient
API reference for createSafeActionClient — configure error handling, middleware, and create reusable type-safe action clients for Nuxt.