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

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)

Basic Usage

Simple Error

// server/api/users/[id].get.ts
import { createError } from 'evlog'

throw createError({
  message: 'User not found',
  status: 404,
})

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

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"
}

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 example for a working implementation.

Next Steps