JavaScript 错误处理机制总结:同步/异步错误,Vue 错误处理

同步错误 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);
}

重要提醒

  1. 全局错误处理是最后防线,不应替代具体的错误处理

  2. 某些框架(如 React、Vue)有自己的错误边界机制

  3. 在 Node.js 中,uncaughtException 捕获后进程可能处于不稳定状态

  4. 现代 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的错误边界那样"开箱即用",但通过组合使用errorCapturederrorHandler和自定义错误边界组件,可以实现同样强大且更灵活的错误处理能力。