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

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

Next Steps

Copyright © 2026