Core Concepts
Structured Errors
Create errors that explain why they occurred and how to fix them. Add actionable context with why, fix, and link fields for humans and AI agents.
evlog provides a createError() function that creates errors with rich, actionable context.
Why Structured Errors?
Traditional errors are often unhelpful:
server/api/checkout.post.ts
// Unhelpful error
throw new Error('Payment failed')
This tells you what happened, but not why or how to fix it.
Structured errors provide context:
// server/api/checkout.post.ts
throw createError({
message: 'Payment failed',
status: 402,
why: 'Card declined by issuer (insufficient funds)',
fix: 'Try a different payment method or contact your bank',
link: 'https://docs.example.com/payments/declined',
})
{
"statusCode": 402,
"message": "Payment failed",
"data": {
"why": "Card declined by issuer (insufficient funds)",
"fix": "Try a different payment method or contact your bank",
"link": "https://docs.example.com/payments/declined"
}
}
Error Fields
| Field | Required | Description |
|---|---|---|
message | Yes | What happened (shown to users) |
status | No | HTTP status code (default: 500) |
why | No | Technical reason (for debugging) |
fix | No | Actionable solution |
link | No | Documentation URL |
cause | No | Original error (for error chaining) |
Basic Usage
Simple Error
// server/api/users/[id].get.ts
import { createError } from 'evlog'
throw createError({
message: 'User not found',
status: 404,
})
{
"statusCode": 404,
"message": "User not found"
}
Error with Full Context
// server/api/checkout.post.ts
throw createError({
message: 'Payment failed',
status: 402,
why: 'Card declined by issuer',
fix: 'Try a different payment method',
link: 'https://docs.example.com/payments/declined',
})
{
"statusCode": 402,
"message": "Payment failed",
"data": {
"why": "Card declined by issuer",
"fix": "Try a different payment method",
"link": "https://docs.example.com/payments/declined"
}
}
Error Chaining
Wrap underlying errors while preserving the original:
server/api/checkout.post.ts
try {
await stripe.charges.create(charge)
} catch (err) {
throw createError({
message: 'Payment processing failed',
status: 500,
why: 'Stripe API returned an error',
cause: err, // Original error preserved
})
}
Frontend Error Handling
Use parseError() to extract all fields from caught errors:
// composables/useCheckout.ts
import { parseError } from 'evlog'
try {
await $fetch('/api/checkout', { method: 'POST', body: cart })
} catch (err) {
const error = parseError(err)
console.log(error.message) // "Payment failed"
console.log(error.status) // 402
console.log(error.why) // "Card declined"
console.log(error.fix) // "Try another card"
}
// composables/useCheckout.ts
import { parseError } from 'evlog'
const toast = useToast()
try {
await $fetch('/api/checkout', { method: 'POST', body: cart })
} catch (err) {
const error = parseError(err)
toast.add({
title: error.message,
description: error.why,
color: 'error',
actions: error.link
? [{ label: 'Learn more', onClick: () => window.open(error.link) }]
: undefined,
})
}
Error Display Component
Create a reusable error display:
components/ErrorAlert.vue
<script setup lang="ts">
import { parseError } from 'evlog'
const { error } = defineProps<{
error: unknown
}>()
const parsed = computed(() => parseError(error))
</script>
<template>
<UAlert
:title="parsed.message"
:description="parsed.why"
color="error"
icon="i-lucide-alert-circle"
>
<template v-if="parsed.fix" #description>
<p>{{ parsed.why }}</p>
<p class="mt-2 font-medium">{{ parsed.fix }}</p>
</template>
</UAlert>
</template>
Best Practices
Use Appropriate Status Codes
// Client error - user can fix
throw createError({
message: 'Invalid email format',
status: 400,
fix: 'Please enter a valid email address',
})
// Authentication required
throw createError({
message: 'Please log in to continue',
status: 401,
fix: 'Sign in to your account',
link: '/login',
})
// Resource not found
throw createError({
message: 'Order not found',
status: 404,
})
// Server error - not user's fault
throw createError({
message: 'Something went wrong',
status: 500,
why: 'Database connection timeout',
// No 'fix' - user can't fix server errors
})
Provide Actionable Fixes
// Unhelpful fix
throw createError({
message: 'Upload failed',
fix: 'Try again',
})
// Actionable fix
throw createError({
message: 'Upload failed',
status: 413,
why: 'File exceeds maximum size (10MB)',
fix: 'Reduce the file size or compress the image before uploading',
link: '/docs/upload-limits',
})
Error Categories
Consider creating factory functions for common error types:
// server/utils/errors.ts
import { createError } from 'evlog'
export const errors = {
notFound: (resource: string) =>
createError({
message: `${resource} not found`,
status: 404,
}),
unauthorized: () =>
createError({
message: 'Please log in to continue',
status: 401,
fix: 'Sign in to your account',
}),
validation: (field: string, issue: string) =>
createError({
message: `Invalid ${field}`,
status: 400,
why: issue,
fix: `Please provide a valid ${field}`,
}),
}
// server/api/orders/[id].get.ts
import { errors } from '~/server/utils/errors'
export default defineEventHandler(async (event) => {
const order = await getOrder(event.context.params.id)
if (!order) {
throw errors.notFound('Order')
}
return order
})
See the Next.js example for a working implementation.
Next Steps
- Quick Start - See all evlog APIs in action