Logging

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:

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',
})

Error Fields

FieldRequiredDescription
messageYesWhat happened (shown to users)
statusNoHTTP status code (default: 500)
whyNoTechnical reason (for debugging)
fixNoActionable solution
linkNoDocumentation URL
causeNoOriginal error (for error chaining)
internalNoBackend-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() omit internal.
  • parseError() does not surface internal for UI; the thrown error may still carry it server-side on raw when debugging.
  • Wide events: when the framework records the error (e.g. log.error(err) or automatic capture on thrown EvlogError), the emitted payload includes error.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,
})

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',
})

Error Chaining

Wrap underlying errors while preserving the original:

server/api/checkout.post.ts
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"
}

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',
})

Provide Actionable Fixes

// Unhelpful fix
throw createError({
  message: 'Upload failed',
  fix: 'Try again',
})

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}`,
    }),
}
See the Next.js guide for a working implementation.

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