Structured Errors
evlog provides a createError() function that creates errors with rich, actionable context.
Why Structured Errors?
Traditional errors are often unhelpful:
// Unhelpful error
throw new Error('Payment failed')
This tells you what happened, but not why or how to fix it.
Structured errors provide context:
import { createError } from 'evlog'
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
import { createError } from 'evlog'
throw createError({
message: 'User not found',
status: 404,
})
{
"statusCode": 404,
"message": "User not found"
}
Error with Full Context
import { createError } from 'evlog'
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:
import { createError } from 'evlog'
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:
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"
}
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:
<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
- Wide Events: Accumulate context and emit comprehensive events
- Adapters: Send errors and events to Axiom, Sentry, PostHog, and more
- Frameworks: Auto-managed request logging per framework
- Quick Start: See all evlog APIs in action
Wide Events
Accumulate context over any unit of work and emit a single comprehensive event. Works for HTTP requests, scripts, background jobs, queue workers, and workflows.
Client Logging
Capture browser events with structured logging. Same API as the server, with automatic console styling, user identity context, and optional server transport.