Structured Errors
evlog provides a createError() function that creates errors with rich, actionable context.
Use structured errors in my app
Why Structured Errors?
throw new Error("Payment failed")
err.message → "Payment failed"
err.status → undefined
err.why → undefined
err.fix → undefined Something went wrong.
Please try again.
throw createError({
message: "Payment failed", // what went wrong
status: 402, // HTTP status
why: "Card declined by issuer", // technical reason
fix: "Try a different card", // actionable advice
link: "/docs/payments/declined" // docs link
}){ message, status, why, fix, link }
all fields available · safe by defaultTraditional 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({
code: 'PAYMENT_DECLINED',
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": {
"code": "PAYMENT_DECLINED",
"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) |
code | No | Stable machine-readable identifier for client branching (e.g. 'PAYMENT_DECLINED') |
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) |
internal | No | Backend-only context (see below) |
Backend-only context (internal)
Use internal when you need extra fields for logs, drains, or support tools, but must not expose them in API responses or to parseError() on the client.
throw createError({
message: 'Payment could not be completed',
status: 402,
why: 'Your card was declined',
fix: 'Try another payment method',
internal: {
correlationId: 'pay_8x2k',
processorCode: 'insufficient_funds',
rawIssuerResponse: '…', // never sent to the client
},
})
- HTTP responses (Nuxt/Nitro error handler, Next.js, SvelteKit, etc.) and
toJSON()omitinternal. parseError()does not surfaceinternalfor UI; the thrown error may still carry it server-side onrawwhen debugging.- Wide events: when the framework records the error (e.g.
log.error(err)or automatic capture on thrownEvlogError), the emitted payload includeserror.internal.
In debuggers, the payload may appear under a symbol key; in code, always use error.internal.
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
})
}
Branching on code
code is a stable, machine-readable identifier you control. Pair it with parseError() so the client can branch on logic without parsing user-facing messages or coupling to HTTP status codes.
import { parseError } from 'evlog'
try {
await $fetch('/api/checkout', { method: 'POST', body: cart })
} catch (err) {
const error = parseError(err)
switch (error.code) {
case 'PAYMENT_DECLINED':
return showRetryWithDifferentCard()
case 'CART_EXPIRED':
return rebuildCart()
default:
return toast.add({ title: error.message, color: 'error' })
}
}
parseError() also surfaces code from Node-style errors (e.g. 'ENOENT', 'ECONNRESET') and any Error instance with a string .code property, so existing system errors flow through the same branch.
code is also copied onto wide events under error.code, so dashboards and drains can group, alert, and chart by code without parsing free-text messages.
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.code) // "PAYMENT_DECLINED"
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.