This boilerplate includes a comprehensive error handling system with custom error classes and centralized handlers for consistent error handling across both server and client.
The error handling system provides:
Located in shared/errors/custom-errors.ts, these type-safe error classes can be used throughout your application:
throw new NotFoundError('Product')
throw new ValidationError('Invalid email', validationDetails)
throw new UnauthorizedError('Login required')
throw new ForbiddenError('Insufficient permissions')
throw new ConflictError('Email already exists')
throw new InternalServerError('Database connection failed')
| Error class | Status code | When to use |
|---|---|---|
ValidationError | 400 | Invalid input data |
UnauthorizedError | 401 | Authentication required |
ForbiddenError | 403 | Insufficient permissions |
NotFoundError | 404 | Resource doesn't exist |
ConflictError | 409 | Resource already exists |
InternalServerError | 500 | Unexpected server errors |
The error handler (server/middleware/99-error-handler.ts) automatically catches all unhandled errors and transforms them into consistent HTTP responses. This means you don't need try-catch blocks in most API routes.
All API errors follow this format:
{
statusCode: 404,
statusMessage: "Product not found",
data: {
code: "NOT_FOUND",
message: "Product not found",
details: { /* Only in development */ }
}
}
Throw custom errors for business logic violations:
import { NotFoundError } from '@@/shared/errors/custom-errors'
import prisma from '@@/lib/prisma'
export async function getProduct(id: string) {
const product = await prisma.product.findUnique({
where: { id },
})
if (!product) {
throw new NotFoundError('Product')
}
return product
}
Database errors and other unexpected errors automatically bubble up to the middleware, which logs them and returns appropriate responses.
Validate input with Zod, then call services:
import { ValidationError } from '@@/shared/errors/custom-errors'
import { createProductSchema } from '@@/shared/schemas'
export default defineEventHandler(async event => {
const body = await readBody(event)
const parseResult = createProductSchema.safeParse(body)
if (!parseResult.success) {
throw new ValidationError('Invalid request data', parseResult.error.issues)
}
const product = await createProduct(parseResult.data)
return product
})
ZodError instances and transforms them into 400 responses, so you can also use .parse() instead of .safeParse() if you prefer.Use useErrorHandler in components for consistent error handling:
const { handleError, handleSuccess } = useErrorHandler()
const handleSubmit = async () => {
try {
await createProduct(formData)
handleSuccess('Product created successfully')
} catch (error) {
handleError(error, 'Failed to create product')
}
}
The composable automatically:
Client services let errors throw - the component layer handles them:
/**
* Create a new product
* Throws error on failure - handle with useErrorHandler composable
*/
export const createProduct = async (data: ProductPayload): Promise<Product> => {
return await $fetch<Product>('/api/products', {
method: 'POST',
body: data,
})
}
<script setup lang="ts">
import { createProduct } from '@@/app/services/products-client-service'
const { handleError, handleSuccess } = useErrorHandler()
const isSubmitting = ref(false)
const formData = reactive({
name: '',
price: 0,
})
const handleSubmit = async () => {
isSubmitting.value = true
try {
await createProduct(formData)
handleSuccess('Product created successfully')
// Reset form, navigate, etc.
} catch (error) {
handleError(error, 'Failed to create product')
} finally {
isSubmitting.value = false
}
}
</script>
// Server service
export async function getProducts() {
return await prisma.product.findMany()
}
// API route
export default defineEventHandler(async () => {
return await getProducts()
})
// Client service
export const fetchProducts = async () => {
return await $fetch<Product[]>('/api/products')
}
// Component
const { handleError } = useErrorHandler()
try {
products.value = await fetchProducts()
} catch (error) {
handleError(error, 'Failed to load products')
}
export async function purchaseProduct(userId: string, productId: string) {
const user = await prisma.user.findUnique({ where: { id: userId } })
if (!user) {
throw new NotFoundError('User')
}
if (!user.emailVerified) {
throw new ForbiddenError('Email verification required')
}
const product = await prisma.product.findUnique({ where: { id: productId } })
if (!product) {
throw new NotFoundError('Product')
}
if (product.stock < 1) {
throw new ConflictError('Product out of stock')
}
// Create purchase...
return purchase
}
When you need to handle errors gracefully without aborting:
export async function updateLicenseKeyUsage(licenseKeyId: string): Promise<void> {
try {
await prisma.licenseKey.update({
where: { id: licenseKeyId },
data: {
usageCount: { increment: 1 },
lastUsedAt: new Date(),
},
})
} catch (error) {
logger.error('Error updating license key usage:', error)
// Don't throw - usage tracking failure shouldn't block downloads
}
}
new ValidationError(message: string, details?: unknown)
new UnauthorizedError(message?: string)
new ForbiddenError(message?: string)
new NotFoundError(resource: string)
new ConflictError(message: string)
new InternalServerError(message?: string)
const {
handleError, // (error: unknown, customMessage?: string) => void
handleSuccess, // (message?: string) => void
} = useErrorHandler()
The error handling system automatically sanitizes error messages in production, hiding stack traces and sensitive details while logging full error information server-side.
For production error monitoring, consider integrating services like Sentry, LogRocket, or Datadog. The error handler middleware can be extended to send errors to these services:
import * as Sentry from '@sentry/node'
export const onError = (error: unknown) => {
// Existing error handling...
if (process.env.NODE_ENV === 'production') {
Sentry.captureException(error)
}
// Return error response...
}
shared/errors/custom-errors.ts - Custom error class definitionsserver/middleware/99-error-handler.ts - Global server error handlerapp/composables/use-error-handler.ts - Client error handler composable