同步错误 vs 异步错误
1. 同步错误(需要 try-catch)
javascript
javascript
function syncFunction() {
throw new Error('同步错误'); // ❌ 不捕获会崩溃
}
// 如果没有 try-catch,程序会崩溃
try {
syncFunction();
} catch (error) {
console.log('捕获到同步错误:', error);
}
2. 异步错误(无法用 try-catch 直接捕获)
javascript
javascript
async function asyncFunction() {
throw new Error('异步错误');
}
// ❌ 这无法捕获异步错误
try {
asyncFunction();
} catch (error) {
// 这里永远不会执行
console.log('不会执行这里');
}
// ✅ 正确方式:使用 .catch() 或 await
asyncFunction().catch(error => {
console.log('捕获异步错误:', error);
});
全局错误捕获
JavaScript 确实有全局错误处理机制 ,但这不等于自动捕获:
浏览器环境:
javascript
javascript
// 1. window.onerror
window.onerror = function(message, source, lineno, colno, error) {
console.log('全局错误:', message);
return true; // 阻止默认错误提示
};
// 2. unhandledrejection(捕获未处理的 Promise 拒绝)
window.addEventListener('unhandledrejection', event => {
console.log('未处理的 Promise 拒绝:', event.reason);
});
Node.js 环境:
javascript
javascript
// 1. process 全局捕获
process.on('unhandledRejection', (reason, promise) => {
console.log('未处理的 Promise 拒绝:', reason);
});
process.on('uncaughtException', (error) => {
console.log('未捕获的异常:', error);
// 注意:这里捕获后进程仍会继续,可能不稳定
});
关键结论
| 场景 | 是否自动捕获 | 说明 |
|---|---|---|
| 同步代码 | ❌ 不会自动捕获 | 需要 try-catch,否则程序崩溃 |
| 异步代码 | ❌ 不会自动捕获 | try-catch 无效,需要 .catch() |
| 全局未捕获错误 | ✅ 可配置捕获 | 通过全局事件监听,但只是最后防线 |
最佳实践
javascript
javascript
// 1. 同步代码:使用 try-catch
function riskySyncOperation() {
try {
// 可能出错的代码
JSON.parse('无效 JSON');
} catch (error) {
// 处理错误
console.error('解析失败:', error);
// 或重新抛出
throw new Error('处理数据时出错', { cause: error });
}
}
// 2. 异步代码:使用 async/await + try-catch
async function riskyAsyncOperation() {
try {
const data = await fetchData();
const result = await processData(data);
return result;
} catch (error) {
console.error('异步操作失败:', error);
// 返回默认值或重新抛出
return null;
}
}
// 3. Promise:使用 .catch()
fetchData()
.then(processData)
.catch(error => {
console.error('Promise 链错误:', error);
});
// 4. 设置全局后备方案
if (typeof window !== 'undefined') {
window.addEventListener('error', handleGlobalError);
window.addEventListener('unhandledrejection', handleUnhandledRejection);
}
重要提醒
-
全局错误处理是最后防线,不应替代具体的错误处理
-
某些框架(如 React、Vue)有自己的错误边界机制
-
在 Node.js 中,
uncaughtException捕获后进程可能处于不稳定状态 -
现代 JavaScript 的
async/await让错误处理更接近同步风格
总结:JavaScript 不会自动捕获和处理错误,你需要主动处理它们,否则程序会崩溃或进入不可预测状态。
Vue 错误边界机制详解
概述
Vue 没有像 React 16+ 那样内置的正式"错误边界"(Error Boundary)概念,但提供了一系列错误处理机制来实现类似功能。
Vue 错误处理机制分类
| 机制 | 作用范围 | 使用方式 | 捕获的错误类型 | 生命周期 |
|---|---|---|---|---|
| errorCaptured 钩子 | 组件树向上传播 | 组件选项 | 子孙组件所有错误 | 捕获时 |
| errorHandler 全局 | 整个应用 | Vue.config | 所有未处理错误 | 全局配置 |
| warnHandler 全局 | 整个应用 | Vue.config | 警告信息 | 全局配置 |
| renderError | 单个组件 | 组件选项 | 渲染函数错误 | 替代渲染 |
| window.onerror | 全局 | 浏览器API | 全局未捕获错误 | 兜底处理 |
| Promise 错误 | 异步操作 | unhandledrejection | Promise拒绝未处理 | 事件监听 |
详细机制说明
1. errorCaptured 生命周期钩子(最接近React错误边界)
javascript
// 父组件中定义错误边界组件
export default {
name: 'ErrorBoundary',
data() {
return {
error: null,
errorInfo: null
}
},
// 捕获所有子孙组件的错误
errorCaptured(err, vm, info) {
// err: 错误对象
// vm: 发生错误的组件实例
// info: Vue特定的错误信息,如生命周期钩子名称
console.error('错误被捕获:', err);
console.error('组件:', vm);
console.error('位置:', info);
// 1. 阻止错误继续向上传播
// return false;
// 2. 记录错误状态
this.error = err;
this.errorInfo = info;
// 3. 可以在这里上报错误到监控系统
this.reportError(err);
// 返回false阻止错误继续向上冒泡
return false;
},
methods: {
reportError(error) {
// 发送到错误监控服务
if (process.env.NODE_ENV === 'production') {
sendToMonitoringService(error);
}
},
resetError() {
this.error = null;
this.errorInfo = null;
this.$forceUpdate();
}
},
render(h) {
if (this.error) {
// 显示错误UI
return h('div', { class: 'error-boundary' }, [
h('h2', '组件出错啦!'),
h('p', this.error.toString()),
h('button', {
on: {
click: this.resetError
}
}, '重试')
]);
}
// 正常渲染子组件
return this.$slots.default ? this.$slots.default[0] : null;
}
}
2. 全局错误处理配置
javascript
// main.js 或应用入口文件
import Vue from 'vue';
// 1. 全局错误处理器
Vue.config.errorHandler = function (err, vm, info) {
// 处理所有未被errorCaptured捕获的错误
console.error('全局错误捕获:', err);
console.error('发生在组件:', vm.$options.name);
console.error('错误信息:', info);
// 生产环境错误上报
if (process.env.NODE_ENV === 'production') {
trackError(err, {
component: vm.$options.name,
info: info,
route: vm.$route?.path
});
}
};
// 2. 全局警告处理器
Vue.config.warnHandler = function (msg, vm, trace) {
// 处理Vue的警告信息
console.warn('Vue警告:', msg);
console.warn('组件追踪:', trace);
};
// 3. 关闭生产提示
Vue.config.productionTip = false;
// 4. 忽略某些自定义元素(如Web Components)
Vue.config.ignoredElements = [/^app-/];
3. renderError 组件级错误处理(Vue 2.6.0+)
javascript
<template>
<div>
<slot v-if="!hasError"></slot>
<div v-else class="error-fallback">
<h3>渲染错误</h3>
<button @click="retry">重试</button>
</div>
</div>
</template>
<script>
export default {
name: 'RenderErrorBoundary',
data() {
return {
hasError: false
};
},
// 捕获渲染函数中的错误
renderError(h, err) {
// 只在开发环境显示详细错误
if (process.env.NODE_ENV !== 'production') {
return h('pre', { style: { color: 'red' } }, err.stack);
}
return h('div', '渲染出错,请刷新页面');
},
methods: {
retry() {
this.hasError = false;
// 强制重新渲染
this.$forceUpdate();
}
},
errorCaptured(err, vm, info) {
if (info === 'render function') {
this.hasError = true;
// 阻止错误继续传播
return false;
}
}
};
</script>
完整错误边界组件实现
错误边界组件(ErrorBoundary.vue)
javascript
<template>
<div class="error-boundary">
<slot v-if="!error" name="default"></slot>
<div v-else class="error-container">
<!-- 错误显示 -->
<div class="error-content">
<h3 class="error-title">
<icon-warning />
组件加载失败
</h3>
<p class="error-message" v-if="!isProduction">
{{ errorMessage }}
</p>
<div class="error-actions">
<button class="btn-retry" @click="handleRetry">
重试
</button>
<button class="btn-report" @click="handleReport" v-if="!isProduction">
报告错误
</button>
<button class="btn-back" @click="handleBack" v-if="hasRouter">
返回首页
</button>
</div>
<!-- 开发环境显示堆栈 -->
<details class="error-details" v-if="!isProduction">
<summary>错误详情</summary>
<pre class="error-stack">{{ errorStack }}</pre>
<pre class="component-info" v-if="errorInfo">{{ errorInfo }}</pre>
</details>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ErrorBoundary',
props: {
// 错误重试次数
maxRetries: {
type: Number,
default: 3
},
// 是否自动重试
autoRetry: {
type: Boolean,
default: false
},
// 重试延迟(毫秒)
retryDelay: {
type: Number,
default: 1000
},
// 自定义错误回调
onError: {
type: Function,
default: null
}
},
data() {
return {
error: null,
errorInfo: null,
errorComponent: null,
retryCount: 0,
isProduction: process.env.NODE_ENV === 'production'
};
},
computed: {
errorMessage() {
if (!this.error) return '';
return this.error.message || this.error.toString();
},
errorStack() {
if (!this.error) return '';
return this.error.stack || '无堆栈信息';
},
hasRouter() {
return !!this.$router;
}
},
errorCaptured(err, vm, info) {
console.error('ErrorBoundary捕获到错误:', err);
// 记录错误信息
this.error = err;
this.errorInfo = info;
this.errorComponent = vm;
// 调用自定义错误处理
if (this.onError) {
this.onError(err, vm, info);
}
// 生产环境错误上报
if (this.isProduction) {
this.reportError(err, vm, info);
}
// 自动重试逻辑
if (this.autoRetry && this.retryCount < this.maxRetries) {
setTimeout(() => {
this.handleRetry();
}, this.retryDelay);
}
// 阻止错误继续向上传播
return false;
},
methods: {
handleRetry() {
if (this.retryCount >= this.maxRetries) {
console.warn(`已达到最大重试次数: ${this.maxRetries}`);
return;
}
this.retryCount++;
console.log(`第 ${this.retryCount} 次重试...`);
// 清空错误状态
this.error = null;
this.errorInfo = null;
this.errorComponent = null;
// 强制重新渲染子组件
this.$forceUpdate();
// 触发重试事件
this.$emit('retry', this.retryCount);
},
handleReport() {
// 错误上报逻辑
const errorData = {
message: this.errorMessage,
stack: this.errorStack,
component: this.errorComponent?.$options.name || 'unknown',
info: this.errorInfo,
url: window.location.href,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent
};
// 发送到错误收集服务
this.sendErrorReport(errorData);
this.$emit('reported', errorData);
alert('错误报告已发送,感谢您的反馈!');
},
handleBack() {
if (this.$router) {
this.$router.push('/');
}
},
reportError(err, vm, info) {
// 集成第三方错误监控
if (window.Sentry) {
window.Sentry.captureException(err, {
extra: {
component: vm?.$options.name,
info: info
}
});
}
// 或发送到自定义监控服务
this.sendToAnalytics(err);
},
sendErrorReport(data) {
// 实际项目中替换为真实的API调用
fetch('/api/error-report', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
}).catch(console.error);
},
sendToAnalytics(error) {
// 发送到分析平台
if (window.ga) {
window.ga('send', 'exception', {
exDescription: error.message,
exFatal: true
});
}
}
},
mounted() {
// 全局未处理Promise错误
window.addEventListener('unhandledrejection', (event) => {
console.error('未处理的Promise拒绝:', event.reason);
this.error = event.reason;
event.preventDefault(); // 阻止控制台默认错误
});
},
beforeDestroy() {
window.removeEventListener('unhandledrejection', this.handleUnhandledRejection);
}
};
</script>
<style scoped>
.error-boundary {
width: 100%;
height: 100%;
}
.error-container {
display: flex;
align-items: center;
justify-content: center;
min-height: 200px;
padding: 20px;
border: 1px solid #f0f0f0;
border-radius: 8px;
background: #fff;
}
.error-content {
text-align: center;
max-width: 500px;
}
.error-title {
color: #f56c6c;
margin-bottom: 16px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.error-message {
color: #606266;
margin-bottom: 24px;
font-size: 14px;
}
.error-actions {
display: flex;
gap: 12px;
justify-content: center;
margin-bottom: 24px;
}
.error-actions button {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.btn-retry {
background: #409eff;
color: white;
}
.btn-retry:hover {
background: #66b1ff;
}
.btn-report {
background: #e6a23c;
color: white;
}
.btn-report:hover {
background: #ebb563;
}
.btn-back {
background: #67c23a;
color: white;
}
.btn-back:hover {
background: #85ce61;
}
.error-details {
margin-top: 20px;
text-align: left;
border-top: 1px solid #eee;
padding-top: 20px;
}
.error-details summary {
cursor: pointer;
color: #909399;
margin-bottom: 10px;
}
.error-stack,
.component-info {
background: #f5f5f5;
padding: 10px;
border-radius: 4px;
font-size: 12px;
white-space: pre-wrap;
word-break: break-all;
max-height: 200px;
overflow-y: auto;
color: #666;
}
.component-info {
margin-top: 10px;
background: #f0f9eb;
}
</style>
使用示例
javascript
<template>
<div id="app">
<!-- 全局错误边界 -->
<error-boundary :max-retries="3" @retry="onRetry">
<router-view />
</error-boundary>
<!-- 局部错误边界 -->
<error-boundary v-if="useErrorBoundary">
<async-component :data="dynamicData" />
</error-boundary>
<!-- 多个独立边界 -->
<div class="dashboard">
<error-boundary>
<chart-component />
</error-boundary>
<error-boundary>
<data-table />
</error-boundary>
<error-boundary>
<user-widget />
</error-boundary>
</div>
</div>
</template>
<script>
import ErrorBoundary from './components/ErrorBoundary.vue';
export default {
components: { ErrorBoundary },
methods: {
onRetry(count) {
console.log(`重试了 ${count} 次`);
// 可以在这里记录重试日志
}
}
};
</script>
Vue 3 中的变化
javascript
// Vue 3 组合式API错误处理
import { onErrorCaptured, ref } from 'vue';
export default {
setup() {
const error = ref(null);
// 类似 errorCaptured 的组合式API
onErrorCaptured((err, instance, info) => {
error.value = err;
console.error('错误:', err);
// 返回 false 阻止继续传播
return false;
});
return { error };
}
};
// Vue 3 应用级错误处理
const app = createApp(App);
app.config.errorHandler = (err, vm, info) => {
// 处理错误
};
最佳实践总结
| 实践 | 建议 | 说明 |
|---|---|---|
| 分层处理 | 组件级 + 全局级 | 组件级处理具体错误,全局级兜底 |
| 错误上报 | 生产环境必须 | 集成Sentry/Bugsnag等监控 |
| 用户友好 | 提供重试/反馈 | 不要让用户面对空白页面 |
| 开发体验 | 详细错误信息 | 开发环境显示堆栈,生产环境简洁 |
| 性能考虑 | 避免无限重试 | 设置最大重试次数和延迟 |
| 路由集成 | 处理导航错误 | 配合Vue Router的错误处理 |
与React错误边界的对比
| 特性 | Vue | React |
|---|---|---|
| 内置组件 | 无,需自己实现 | 有,ErrorBoundary组件 |
| 错误捕获 | errorCaptured钩子 | componentDidCatch生命周期 |
| 传播控制 | return false阻止 | 自动停止,无显式控制 |
| 渲染降级 | 需手动实现 | getDerivedStateFromError自动 |
| 组合使用 | 更灵活,可嵌套 | 较固定,按组件树结构 |
Vue的错误处理机制虽然不如React的错误边界那样"开箱即用",但通过组合使用errorCaptured、errorHandler和自定义错误边界组件,可以实现同样强大且更灵活的错误处理能力。