Error handling
This boilerplate includes a comprehensive error handling system with custom error classes and centralized handlers for consistent error handling across both server and client.
Overview
The error handling system provides:
- Custom error classes - Type-safe errors with HTTP status codes
- Server middleware - Automatic error transformation
- Client composable - Consistent error handling with toast notifications
- Security - Sanitized error messages in production
Custom error classes
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')
Available error types
| 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 |
Server-side error handling
Global error middleware
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 */ }
}
}
Usage in server services
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.
Usage in API routes
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.Client-side error handling
Error handler composable
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:
- Parses server error responses
- Shows toast notifications to users
- Logs detailed errors to console (development only)
- Provides user-friendly messages based on status codes
Client services
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,
})
}
Component example
<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>
Common patterns
Simple resource operations
// 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')
}
Business logic with validation
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
}
Operations with fallbacks
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
}
}
API reference
Custom error classes
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)
Error handler composable
const {
handleError, // (error: unknown, customMessage?: string) => void
handleSuccess, // (message?: string) => void
} = useErrorHandler()
Production considerations
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...
}
Related files
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