日志记录

结构化错误

创建能够解释发生原因和解决方法的错误。为人类和 AI 代理添加包含 why、fix 和 link 字段的可操作上下文。

evlog 提供了 createError() 函数,用于创建带有丰富、可操作上下文的错误。

为什么需要结构化错误?

传统错误通常没有帮助:

server/api/checkout.post.ts
// 缺乏帮助的错误
throw new Error('Payment failed')

这只能告诉你发生了什么,而不是为什么发生如何修复

结构化错误提供了上下文信息:

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

错误字段

字段必需描述
message发生了什么(展示给用户)
statusHTTP 状态码(默认:500)
why技术原因(用于调试)
fix可操作的解决方案
link文档 URL
cause原始错误(用于错误链)
internal仅后端使用的上下文(参见下文)

仅后端使用的上下文(internal

当你需要为日志、排水或支持工具提供额外字段时,请使用 internal,但绝不能在 API 响应或客户端的 parseError() 中暴露这些字段。

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: '', // 不会发送给客户端
  },
})
  • HTTP 响应(Nuxt/Nitro 错误处理器、Next.js、SvelteKit 等)和 toJSON() 会省略 internal
  • parseError() 不会在 UI 中暴露 internal;但在服务端调试时,抛出的错误可能在 raw 中仍携带它。
  • 广泛事件:当框架记录错误时(例如 log.error(err) 或自动捕获抛出的 EvlogError),发出的有效载荷会包含 error.internal

在调试器中,有效载荷可能显示在符号键下;在代码中,始终使用 error.internal

基本用法

简单错误

import { createError } from 'evlog'

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

带有完整上下文的错误

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

错误链

在保留原始错误的同时包装底层错误:

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, // 保留原始错误
  })
}

前端错误处理

使用 parseError() 从捕获的错误中提取所有字段:

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

错误显示组件

创建一个可复用的错误显示组件:

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>

最佳实践

使用合适的状态码

// 客户端错误 - 用户可以修复
throw createError({
  message: 'Invalid email format',
  status: 400,
  fix: 'Please enter a valid email address',
})

提供可操作的修复方案

// 无帮助的修复方案
throw createError({
  message: 'Upload failed',
  fix: 'Try again',
})

错误分类

考虑为常见错误类型创建工厂函数:

// 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.js 指南 了解实际实现。

下一步

  • 广泛事件:累积上下文并发送全面事件
  • 适配器:将错误和事件发送到 Axiom、Sentry、PostHog 等
  • 框架:每个框架自动管理请求日志
  • 快速入门:查看所有 evlog API 的实际效果