Nuxt 错误处理与异常捕获完全指南
在现代 Web 应用中,错误是不可避免的——API 请求可能失败、用户输入可能无效、第三方服务可能宕机。如何优雅地处理这些错误,直接决定了应用的健壮性和用户体验。
Nuxt 3 作为全栈框架,需要同时处理客户端错误和服务端错误,情况更加复杂。本文将系统讲解 Nuxt 的错误处理机制,从基础概念到生产级实践。
错误的分类与特点
在深入实现之前,先理解 Nuxt 中错误的不同类型。
按运行环境分类
| 类型 | 发生环境 | 示例 |
|---|---|---|
| 服务端错误 | Node.js 服务器 | 数据库连接失败、SSR 渲染异常 |
| 客户端错误 | 浏览器 | 事件处理函数异常、组件渲染错误 |
| 混合错误 | 两端都可能 | API 调用失败、数据格式错误 |
按严重程度分类
| 级别 | 描述 | 处理策略 |
|---|---|---|
| 致命错误 | 应用无法继续运行 | 显示错误页面 |
| 可恢复错误 | 局部功能受损 | 降级处理、重试 |
| 可忽略错误 | 不影响核心功能 | 静默上报 |
Nuxt 错误的特殊性
由于 Nuxt 支持 SSR,同一段代码可能在服务端和客户端都执行。这带来了独特的挑战:
// 这段代码在 SSR 时会报错
const width = window.innerWidth // ❌ window 在服务端不存在
// 正确的做法
const width = process.client ? window.innerWidth : 0
error.vue:全局错误页面
Nuxt 提供了一个约定:当发生致命错误时,会渲染项目根目录的 error.vue 组件。
基础错误页面
<!-- error.vue -->
<script setup lang="ts">
import type { NuxtError } from '#app'
const props = defineProps<{
error: NuxtError
}>()
// 清除错误并导航到首页
const handleError = () => {
clearError({ redirect: '/' })
}
</script>
<template>
<div class="error-page">
<div class="error-content">
<h1 class="error-code">{{ error.statusCode }}</h1>
<h2 class="error-title">{{ error.statusMessage || '出错了' }}</h2>
<p class="error-message">{{ error.message }}</p>
<div class="error-actions">
<button @click="handleError" class="btn-primary">
返回首页
</button>
<button @click="$router.back()" class="btn-secondary">
返回上页
</button>
</div>
</div>
</div>
</template>
<style scoped>
.error-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.error-content {
text-align: center;
color: white;
padding: 2rem;
}
.error-code {
font-size: 8rem;
font-weight: 700;
margin: 0;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.error-title {
font-size: 2rem;
margin: 1rem 0;
}
.error-message {
font-size: 1.1rem;
opacity: 0.9;
max-width: 500px;
margin: 0 auto 2rem;
}
.error-actions {
display: flex;
gap: 1rem;
justify-content: center;
}
.btn-primary, .btn-secondary {
padding: 0.75rem 1.5rem;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: transform 0.2s;
}
.btn-primary {
background: white;
color: #667eea;
border: none;
}
.btn-secondary {
background: transparent;
color: white;
border: 2px solid white;
}
.btn-primary:hover, .btn-secondary:hover {
transform: translateY(-2px);
}
</style>
差异化错误页面
根据错误类型显示不同内容:
<!-- error.vue -->
<script setup lang="ts">
import type { NuxtError } from '#app'
const props = defineProps<{
error: NuxtError
}>()
// 错误类型配置
const errorConfig = computed(() => {
const code = props.error.statusCode
const configs: Record<number, {
title: string
message: string
icon: string
}> = {
404: {
title: '页面不存在',
message: '您访问的页面已被移除或从未存在',
icon: '🔍'
},
403: {
title: '访问被拒绝',
message: '您没有权限访问此资源',
icon: '🔒'
},
500: {
title: '服务器错误',
message: '服务器遇到了问题,请稍后重试',
icon: '⚙️'
},
503: {
title: '服务不可用',
message: '服务器正在维护,请稍后访问',
icon: '🔧'
}
}
return configs[code] || {
title: '出错了',
message: props.error.message || '发生了未知错误',
icon: '❌'
}
})
// 开发环境显示详细堆栈
const isDev = process.env.NODE_ENV === 'development'
const handleError = () => {
clearError({ redirect: '/' })
}
</script>
<template>
<div class="error-page">
<div class="error-container">
<div class="error-icon">{{ errorConfig.icon }}</div>
<h1 class="error-code">{{ error.statusCode }}</h1>
<h2 class="error-title">{{ errorConfig.title }}</h2>
<p class="error-description">{{ errorConfig.message }}</p>
<!-- 开发环境显示堆栈 -->
<details v-if="isDev && error.stack" class="error-stack">
<summary>查看错误详情</summary>
<pre>{{ error.stack }}</pre>
</details>
<div class="error-actions">
<button @click="handleError">返回首页</button>
<button @click="() => reloadNuxtApp()">刷新页面</button>
</div>
</div>
</div>
</template>
使用 createError 主动抛出错误
Nuxt 提供 createError 函数来创建格式化的错误对象。
在页面/组件中抛出
// pages/products/[id].vue
<script setup lang="ts">
const route = useRoute()
const { data: product, error } = await useFetch(`/api/products/${route.params.id}`)
// 产品不存在时抛出 404
if (!product.value) {
throw createError({
statusCode: 404,
statusMessage: 'Product Not Found',
message: `找不到 ID 为 ${route.params.id} 的产品`,
fatal: true // 致命错误,显示错误页面
})
}
</script>
在 API 路由中抛出
// server/api/products/[id].ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
const product = await prisma.product.findUnique({
where: { id }
})
if (!product) {
throw createError({
statusCode: 404,
statusMessage: 'Not Found',
message: `产品 ${id} 不存在`
})
}
// 权限检查
const user = event.context.user
if (product.isPrivate && product.ownerId !== user?.id) {
throw createError({
statusCode: 403,
statusMessage: 'Forbidden',
message: '您无权访问此产品'
})
}
return product
})
非致命错误(局部处理)
// 非致命错误不会触发错误页面
const error = createError({
statusCode: 400,
message: '表单验证失败',
fatal: false // 默认值
})
// 可以在组件中局部处理
showError(error)
// 稍后清除
clearError()
showError 与 clearError
这两个函数用于控制错误的显示和清除。
showError:手动触发错误
// 在任何地方触发错误页面
showError({
statusCode: 500,
statusMessage: 'Server Error',
message: '服务暂时不可用'
})
// 等同于
throw createError({ ... })
clearError:清除错误状态
// 清除错误并保持当前页面
clearError()
// 清除错误并重定向
clearError({ redirect: '/' })
// 清除错误并跳转到指定路由
clearError({ redirect: '/login' })
错误状态访问
// 在任意组件中访问当前错误
const error = useError()
// 响应式
watch(error, (newError) => {
if (newError) {
// 上报错误
reportError(newError)
}
})
onErrorCaptured:组件错误边界
Vue 3 的 onErrorCaptured 生命周期可以捕获子组件的错误,实现局部错误边界。
基础错误边界组件
<!-- components/ErrorBoundary.vue -->
<script setup lang="ts">
import { ref, onErrorCaptured } from 'vue'
const props = withDefaults(defineProps<{
fallback?: string
onError?: (error: Error) => void
}>(), {
fallback: '组件加载出错'
})
const error = ref<Error | null>(null)
const errorInfo = ref<string>('')
onErrorCaptured((err, instance, info) => {
error.value = err
errorInfo.value = info
// 调用外部错误处理
props.onError?.(err)
// 返回 false 阻止错误继续向上传播
// 返回 true 或不返回,错误继续传播
return false
})
const retry = () => {
error.value = null
errorInfo.value = ''
}
</script>
<template>
<div v-if="error" class="error-boundary">
<div class="error-fallback">
<p>{{ fallback }}</p>
<button @click="retry">重试</button>
<details v-if="$config.public.isDev">
<summary>错误详情</summary>
<pre>{{ error.message }}</pre>
<pre>{{ errorInfo }}</pre>
</details>
</div>
</div>
<slot v-else />
</template>
<style scoped>
.error-boundary {
padding: 1rem;
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 8px;
}
.error-fallback {
text-align: center;
}
.error-fallback button {
margin-top: 0.5rem;
padding: 0.5rem 1rem;
cursor: pointer;
}
details {
margin-top: 1rem;
text-align: left;
}
pre {
background: #f8f9fa;
padding: 0.5rem;
overflow-x: auto;
font-size: 0.875rem;
}
</style>
使用错误边界
<!-- pages/dashboard.vue -->
<template>
<div class="dashboard">
<h1>控制台</h1>
<!-- 图表组件出错不影响其他部分 -->
<ErrorBoundary fallback="图表加载失败" @error="handleChartError">
<DashboardChart :data="chartData" />
</ErrorBoundary>
<!-- 数据表格 -->
<ErrorBoundary fallback="数据加载失败">
<DataTable :items="tableData" />
</ErrorBoundary>
<!-- 侧边栏 -->
<ErrorBoundary>
<Sidebar />
</ErrorBoundary>
</div>
</template>
<script setup>
const handleChartError = (error) => {
// 上报图表错误
console.error('Chart error:', error)
}
</script>
高级错误边界(支持重试)
<!-- components/ErrorBoundaryAdvanced.vue -->
<script setup lang="ts">
import { ref, onErrorCaptured, provide } from 'vue'
const props = withDefaults(defineProps<{
maxRetries?: number
retryDelay?: number
}>(), {
maxRetries: 3,
retryDelay: 1000
})
const emit = defineEmits<{
error: [error: Error, info: string]
retry: [attempt: number]
maxRetriesReached: []
}>()
const error = ref<Error | null>(null)
const retryCount = ref(0)
const isRetrying = ref(false)
const componentKey = ref(0)
onErrorCaptured((err, instance, info) => {
error.value = err
emit('error', err, info)
// 自动重试逻辑
if (retryCount.value < props.maxRetries) {
autoRetry()
} else {
emit('maxRetriesReached')
}
return false
})
const autoRetry = async () => {
isRetrying.value = true
retryCount.value++
await new Promise(resolve => setTimeout(resolve, props.retryDelay))
emit('retry', retryCount.value)
error.value = null
componentKey.value++
isRetrying.value = false
}
const manualRetry = () => {
retryCount.value = 0
error.value = null
componentKey.value++
}
// 提供给子组件的重试方法
provide('errorBoundaryRetry', manualRetry)
</script>
<template>
<div v-if="error && !isRetrying" class="error-state">
<slot name="error" :error="error" :retry="manualRetry" :retryCount="retryCount">
<div class="default-error">
<p>加载出错</p>
<p v-if="retryCount >= maxRetries">已达最大重试次数</p>
<button @click="manualRetry">手动重试</button>
</div>
</slot>
</div>
<div v-else-if="isRetrying" class="retrying-state">
<slot name="loading">
<p>重试中... ({{ retryCount }}/{{ maxRetries }})</p>
</slot>
</div>
<slot v-else :key="componentKey" />
</template>
API 请求错误处理
在 Nuxt 中,数据获取是最常见的错误来源。
useFetch 错误处理
// 基础错误处理
const { data, error, pending, refresh } = await useFetch('/api/products')
// 监听错误
watch(error, (newError) => {
if (newError) {
// 显示 toast 通知
toast.error(newError.message)
}
})
封装请求工具
// composables/useApi.ts
import type { NuxtError } from '#app'
interface ApiOptions {
showError?: boolean // 是否显示错误提示
throwOnError?: boolean // 是否抛出错误
retries?: number // 重试次数
retryDelay?: number // 重试间隔
}
interface ApiResult<T> {
data: Ref<T | null>
error: Ref<NuxtError | null>
pending: Ref<boolean>
refresh: () => Promise<void>
}
export function useApi<T>(
url: string | (() => string),
options: ApiOptions = {}
): Promise<ApiResult<T>> {
const {
showError = true,
throwOnError = false,
retries = 0,
retryDelay = 1000
} = options
const retryCount = ref(0)
const fetchData = async (): Promise<ApiResult<T>> => {
const result = await useFetch<T>(url, {
onResponseError({ response }) {
const errorMessage = response._data?.message || response.statusText
if (showError) {
// 使用全局 toast 或通知系统
useToast().error(errorMessage)
}
// 记录错误日志
console.error(`API Error [${response.status}]:`, errorMessage)
}
})
// 重试逻辑
if (result.error.value && retryCount.value < retries) {
retryCount.value++
await new Promise(resolve => setTimeout(resolve, retryDelay))
return fetchData()
}
// 抛出错误
if (throwOnError && result.error.value) {
throw createError({
statusCode: result.error.value.statusCode || 500,
message: result.error.value.message
})
}
return result as ApiResult<T>
}
return fetchData()
}
使用封装后的 API
<script setup lang="ts">
// 自动显示错误提示
const { data: products, pending, refresh } = await useApi<Product[]>('/api/products')
// 静默错误(不显示提示)
const { data: stats } = await useApi('/api/stats', {
showError: false
})
// 带重试
const { data: critical } = await useApi('/api/critical-data', {
retries: 3,
retryDelay: 2000
})
</script>
API 路由统一错误格式
// server/utils/response.ts
export interface ApiResponse<T = any> {
success: boolean
data?: T
error?: {
code: string
message: string
details?: any
}
}
export function successResponse<T>(data: T): ApiResponse<T> {
return {
success: true,
data
}
}
export function errorResponse(
code: string,
message: string,
details?: any
): ApiResponse {
return {
success: false,
error: { code, message, details }
}
}
// server/api/products/[id].ts
import { successResponse, errorResponse } from '~/server/utils/response'
export default defineEventHandler(async (event) => {
try {
const id = getRouterParam(event, 'id')
const product = await prisma.product.findUnique({
where: { id }
})
if (!product) {
setResponseStatus(event, 404)
return errorResponse('PRODUCT_NOT_FOUND', `产品 ${id} 不存在`)
}
return successResponse(product)
} catch (error) {
console.error('Product fetch error:', error)
setResponseStatus(event, 500)
return errorResponse('INTERNAL_ERROR', '服务器内部错误')
}
})
全局错误处理插件
通过 Nuxt 插件可以实现全局错误监听。
Vue 错误处理器
// plugins/error-handler.ts
export default defineNuxtPlugin((nuxtApp) => {
// Vue 全局错误处理
nuxtApp.vueApp.config.errorHandler = (error, instance, info) => {
console.error('Vue Error:', error)
console.error('Component:', instance)
console.error('Info:', info)
// 上报到错误监控服务
if (process.client) {
reportToSentry(error, { component: instance?.$options?.name, info })
}
}
// Vue 警告处理(仅开发环境)
nuxtApp.vueApp.config.warnHandler = (msg, instance, trace) => {
console.warn('Vue Warning:', msg)
console.warn('Trace:', trace)
}
// Nuxt 生命周期钩子错误
nuxtApp.hook('app:error', (error) => {
console.error('Nuxt App Error:', error)
})
// 页面渲染错误
nuxtApp.hook('vue:error', (error, instance, info) => {
console.error('Vue Error Hook:', error)
})
})
function reportToSentry(error: any, context: any) {
// 集成 Sentry 或其他错误监控服务
if (window.Sentry) {
window.Sentry.captureException(error, {
extra: context
})
}
}
未捕获异常处理
// plugins/unhandled-errors.client.ts
export default defineNuxtPlugin(() => {
// 未捕获的 Promise 异常
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled Promise Rejection:', event.reason)
// 可选:阻止默认的控制台错误
// event.preventDefault()
// 上报错误
reportError({
type: 'unhandledrejection',
error: event.reason,
timestamp: Date.now()
})
})
// 未捕获的同步异常
window.addEventListener('error', (event) => {
console.error('Uncaught Error:', event.error)
reportError({
type: 'error',
error: event.error,
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
timestamp: Date.now()
})
})
})
function reportError(errorData: any) {
// 发送到错误收集端点
navigator.sendBeacon('/api/errors', JSON.stringify(errorData))
}
服务端错误处理
Server Middleware 错误处理
// server/middleware/error-handler.ts
export default defineEventHandler((event) => {
// 这个 middleware 会在所有请求之前运行
// 可以用于设置错误处理上下文
event.context.requestId = generateRequestId()
event.context.requestStart = Date.now()
})
// server/plugins/error-logger.ts
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('error', async (error, { event }) => {
// 记录服务端错误
console.error('Server Error:', {
requestId: event?.context?.requestId,
path: event?.path,
method: event?.method,
error: error.message,
stack: error.stack
})
// 发送到日志服务
await logToService({
level: 'error',
message: error.message,
stack: error.stack,
context: {
requestId: event?.context?.requestId,
path: event?.path
}
})
})
// 请求结束时的钩子
nitroApp.hooks.hook('afterResponse', (event) => {
const duration = Date.now() - (event.context.requestStart || Date.now())
// 记录慢请求
if (duration > 3000) {
console.warn(`Slow request: ${event.path} took ${duration}ms`)
}
})
})
function generateRequestId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
}
async function logToService(log: any) {
// 发送到日志服务(如 Elasticsearch、CloudWatch 等)
}
API 路由统一错误中间件
// server/utils/api-handler.ts
type ApiHandler<T> = (event: H3Event) => Promise<T>
export function defineApiHandler<T>(handler: ApiHandler<T>) {
return defineEventHandler(async (event) => {
try {
const result = await handler(event)
return {
success: true,
data: result
}
} catch (error: any) {
// 已知的业务错误
if (error.statusCode) {
setResponseStatus(event, error.statusCode)
return {
success: false,
error: {
code: error.statusCode.toString(),
message: error.message
}
}
}
// 未知错误
console.error('API Error:', error)
setResponseStatus(event, 500)
return {
success: false,
error: {
code: 'INTERNAL_ERROR',
message: process.dev ? error.message : '服务器内部错误'
}
}
}
})
}
使用方式:
// server/api/users/[id].ts
export default defineApiHandler(async (event) => {
const id = getRouterParam(event, 'id')
const user = await prisma.user.findUnique({ where: { id } })
if (!user) {
throw createError({
statusCode: 404,
message: '用户不存在'
})
}
return user
})
错误日志与监控
结构化日志系统
// utils/logger.ts
type LogLevel = 'debug' | 'info' | 'warn' | 'error'
interface LogEntry {
level: LogLevel
message: string
timestamp: string
context?: Record<string, any>
error?: {
name: string
message: string
stack?: string
}
}
class Logger {
private context: Record<string, any> = {}
setContext(ctx: Record<string, any>) {
this.context = { ...this.context, ...ctx }
}
private log(level: LogLevel, message: string, data?: any) {
const entry: LogEntry = {
level,
message,
timestamp: new Date().toISOString(),
context: { ...this.context, ...data }
}
if (data instanceof Error) {
entry.error = {
name: data.name,
message: data.message,
stack: data.stack
}
}
// 开发环境输出到控制台
if (process.dev) {
const method = level === 'error' ? 'error' : level === 'warn' ? 'warn' : 'log'
console[method](`[${level.toUpperCase()}]`, message, entry)
}
// 生产环境发送到日志服务
if (process.env.NODE_ENV === 'production') {
this.sendToLogService(entry)
}
}
private async sendToLogService(entry: LogEntry) {
try {
await $fetch('/api/logs', {
method: 'POST',
body: entry
})
} catch (e) {
console.error('Failed to send log:', e)
}
}
debug(message: string, data?: any) {
this.log('debug', message, data)
}
info(message: string, data?: any) {
this.log('info', message, data)
}
warn(message: string, data?: any) {
this.log('warn', message, data)
}
error(message: string, error?: Error | any) {
this.log('error', message, error)
}
}
export const logger = new Logger()
集成 Sentry
// plugins/sentry.client.ts
import * as Sentry from '@sentry/vue'
export default defineNuxtPlugin((nuxtApp) => {
const config = useRuntimeConfig()
if (!config.public.sentryDsn) {
return
}
Sentry.init({
app: nuxtApp.vueApp,
dsn: config.public.sentryDsn,
environment: config.public.environment,
integrations: [
Sentry.browserTracingIntegration({
router: useRouter()
}),
Sentry.replayIntegration()
],
// 采样率
tracesSampleRate: 0.1,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
// 过滤不需要上报的错误
beforeSend(event, hint) {
const error = hint.originalException
// 忽略特定错误
if (error instanceof Error) {
if (error.message.includes('Network Error')) {
return null
}
if (error.message.includes('ResizeObserver')) {
return null
}
}
return event
}
})
// 添加用户信息
const { user } = useUserStore()
if (user) {
Sentry.setUser({
id: user.id,
email: user.email
})
}
})
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
public: {
sentryDsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV
}
}
})
最佳实践总结
错误处理层次
┌─────────────────────────────────────────────────────┐
│ error.vue │
│ (致命错误兜底页面) │
└────────────────────┬────────────────────────────────┘
│
┌────────────────────┴────────────────────────────────┐
│ 全局错误插件 │
│ (Vue errorHandler + Nuxt hooks) │
└────────────────────┬────────────────────────────────┘
│
┌────────────────────┴────────────────────────────────┐
│ ErrorBoundary 组件 │
│ (组件级错误隔离) │
└────────────────────┬────────────────────────────────┘
│
┌────────────────────┴────────────────────────────────┐
│ Try-Catch / Error 处理 │
│ (业务代码级错误处理) │
└─────────────────────────────────────────────────────┘
检查清单
基础配置:
- 创建 error.vue 全局错误页面
- 针对 404、403、500 等常见错误定制显示
- 开发环境显示错误堆栈
组件级处理:
- 为关键组件添加 ErrorBoundary
- 实现组件级错误降级方案
- 支持错误重试机制
API 错误:
- 封装统一的请求错误处理
- API 返回统一的错误格式
- 实现请求重试逻辑
监控上报:
- 集成错误监控服务(Sentry 等)
- 实现结构化日志记录
- 设置错误告警机制
用户体验:
- 错误提示友好且可操作
- 提供返回/重试等恢复选项
- 避免暴露敏感技术信息
总结
Nuxt 的错误处理涉及多个层面:
- error.vue:全局致命错误的兜底页面
- createError / showError:主动抛出格式化错误
- ErrorBoundary:组件级错误隔离
- 插件钩子:全局错误监听与上报
- API 错误处理:请求层面的统一处理
核心原则是:让错误可预测、可追踪、可恢复。
通过合理的错误处理架构,既能保证应用的健壮性,又能在出错时给用户提供良好的体验,同时为开发团队提供足够的调试信息。


