Core Concepts
Structured Errors
Create errors that explain why they occurred and how to fix them.
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
})
Next Steps
- Quick Start - See all evlog APIs in action