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',
})
{
"statusCode": 402,
"message": "Payment failed",
"data": {
"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 | 是 | 发生了什么(展示给用户) |
status | 否 | HTTP 状态码(默认: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,
})
{
"statusCode": 404,
"message": "User not found"
}
带有完整上下文的错误
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"
}
}
错误链
在保留原始错误的同时包装底层错误:
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"
}
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,
})
}
错误显示组件
创建一个可复用的错误显示组件:
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: 'Please log in to continue',
status: 401,
fix: 'Sign in to your account',
link: '/login',
})
// 资源未找到
throw createError({
message: 'Order not found',
status: 404,
})
// 服务器错误 - 不是用户的错
throw createError({
message: 'Something went wrong',
status: 500,
why: 'Database connection timeout',
// 不提供 'fix' - 用户无法修复服务器错误
})
提供可操作的修复方案
// 无帮助的修复方案
throw createError({
message: 'Upload failed',
fix: 'Try again',
})
// 可操作的修复方案
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',
})
错误分类
考虑为常见错误类型创建工厂函数:
// 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.js 指南 了解实际实现。