概念理解

错误捕获钩子(errorCaptured):Vue 提供的生命周期钩子,用于捕获子组件、子组件生命周期钩子或事件处理函数中抛出的错误。当子组件发生错误时,父组件的 errorCaptured 钩子会被调用,可以决定是否阻止错误继续向上传播。

全局错误处理(errorHandler):Vue 提供的全局配置选项,用于处理所有未被 errorCaptured 捕获的错误。这是错误处理的最后一道防线,如果全局错误处理也失败,错误会被输出到控制台。

错误传播机制:采用向上冒泡的机制,错误会从子组件向父组件逐级传播,直到被某个组件的 errorCaptured 钩子捕获(返回 false)或到达全局错误处理。

应用场景

  • 组件调试:开发时通过捕获错误并阻止传递来排查组件错误来源。
  • 日志上报:实际生产环境下会通过 errorHandler 来收集组件错误信息并上报日志系统。
  • 优雅降级:当子组件渲染错误时,通过组件内错误钩子 errorCaptured 可以用来展示其他 UI 信息,例如异常 UI 等。

Vue 2 错误钩子

// 组件内
export default {
  errorCaptured(err, vm, info) {
    return false; // 阻止传播
  },
};

// 全局钩子
Vue.config.errorHandler = (err, vm, info) => {};

Vue 3 错误钩子

// 组件内钩子
<script setup>
import { onErrorCaptured } from 'vue'


onErrorCaptured((err, instance, info) => {
  // instance 是公共对象,不是 Vue 2 直接把 vm 实例暴露出来
  return false
})
</script>

// 全局错误钩子
const app = createApp(xxx)
app.config.errorHandler = (err, instance, info) => {
  console.log('Global errorHandler:', err.message)
}

Vue 2 错误处理机制

错误捕获流程

Vue 2 的错误处理流程如下:

  1. 组件出错时会触发 handleError,此时先通过 $parent 往父级查找钩子函数。
  2. 遍历执行组件相关的 errorCaptured 钩子函数。
  3. 如果其中之一的 errorCaptured 钩子函数返回 false,那么停止后续的错误捕获,包括全局和组件内的错误钩子。
  4. errorCaptured 执行过程中报错或主动抛错,globalHandleError 会触发 errorHandler 全局错误处理钩子函数去处理 errorCaptured hook
  5. 在完成所有遍历后,如果没有停止错误捕获,最终还是会执行全局钩子 errorHandler,这次是组件自身的错误捕获。
  6. 如果全局 errorHandler 出错,最后会把错误抛给 console.error 输出。

源码实现

// src/core/util/error.ts
export function handleError(err: Error, vm: any, info: string) {
  // See: https://github.com/vuejs/vuex/issues/1505
  // 避免错误钩子无限错误渲染
  pushTarget();

  try {
    if (vm) {
      let cur = vm;
      // 遍历父级,找到首个组件内错误钩子
      while ((cur = cur.$parent)) {
        // 获取对应的钩子数组
        const hooks = cur.$options.errorCaptured;
        // 遍历钩子执行
        if (hooks) {
          for (let i = 0; i < hooks.length; i++) {
            try {
              // 判断是否要捕获错误,捕获那么停止后续的错误函数执行
              // capture 会阻止后续组件内钩子和全局钩子捕获错误
              const capture = hooks[i].call(cur, err, vm, info) === false;
              if (capture) return;
            } catch (e: any) {
              // 假如内部抛错,直接执行全局钩子函数
              globalHandleError(e, cur, "errorCaptured hook");
            }
          }
        }
      }
    }
    // 如果没有捕获,那最终还是会传递给全局钩子
    globalHandleError(err, vm, info);
  } finally {
    popTarget();
  }
}

// 全局错误钩子执行
function globalHandleError(err, vm, info) {
  if (config.errorHandler) {
    try {
      // info 包括组件来源、钩子错误来源等
      return config.errorHandler.call(null, err, vm, info);
    } catch (e: any) {
      // 确定错误来源不是外部传入的 err
      if (e !== err) {
        logError(e, null, "config.errorHandler");
      }
    }
  }
  logError(err, vm, info);
}

// 捕获到特别异常错误,无法由 Vue 处理,直接交给控制台
function logError(err, vm, info) {
  if (__DEV__) {
    warn(`Error in ${info}: "${err.toString()}"`, vm);
  }
  /* istanbul ignore else */
  if (inBrowser && typeof console !== "undefined") {
    console.error(err);
  } else {
    throw err;
  }
}

Vue 3 错误处理机制

Vue 3 和 Vue 2 的错误处理机制类似,都是:捕获钩子 → 全局捕获钩子 → console.error 输出。

主要区别在于:

  • Vue 3 添加了结构化错误枚举类型,整合了更多的错误信息,代替了 Vue 2 的字符串语义。
  • Vue 3 解耦了异步函数和同步函数的错误处理逻辑。
  • Vue 2 中,假如 errorHandler() 被定义并捕获了全局错误,console.error 还是会输出,但 Vue 3 中 errorHandler() 会拦截后续 console.error 输出。
  • Vue 2 是传入 vm 实例来获取钩子,Vue 3 区分了公共实例和内部实例,公共实例需要通过 instance.proxy 来访问。

错误类型枚举

Vue 3 使用枚举来标识不同类型的错误:

// packages/runtime-core/src/errorHandling.ts
export enum ErrorCodes {
  SETUP_FUNCTION,
  RENDER_FUNCTION,
  NATIVE_EVENT_HANDLER = 5,
  COMPONENT_EVENT_HANDLER,
  VNODE_HOOK,
  DIRECTIVE_HOOK,
  TRANSITION_HOOK,
  APP_ERROR_HANDLER,
  APP_WARN_HANDLER,
  FUNCTION_REF,
  ASYNC_COMPONENT_LOADER,
  SCHEDULER,
  COMPONENT_UPDATE,
  APP_UNMOUNT_CLEANUP,
}

export type ErrorTypes = LifecycleHooks | ErrorCodes | WatchErrorCodes;

export const ErrorTypeStrings: Record<ErrorTypes, string> = {
  [ErrorCodes.SETUP_FUNCTION]: "setup function",
  [ErrorCodes.RENDER_FUNCTION]: "render function",
  [ErrorCodes.NATIVE_EVENT_HANDLER]: "native event handler",
  [ErrorCodes.COMPONENT_EVENT_HANDLER]: "component event handler",
  [LifecycleHooks.ERROR_CAPTURED]: "errorCaptured hook",
  // ... 其他错误类型
};

错误处理核心函数

和 Vue 2 类似,Vue 2 采用 invokeWithErrorHandling() 包装函数调用,但是把 Promise 和普通函数的处理耦合在一起,Vue 3 将两种情形的处理进行了解耦。

Vue 2 早期版本没有处理生命周期、watcher 里的异步任务,在 vue@2.7.x 版本补齐了异步错误处理。

callWithErrorHandling

Vue 3 用于包装函数调用的代码更简洁:

export function callWithErrorHandling(
  fn: Function,
  instance: ComponentInternalInstance | null | undefined,
  type: ErrorTypes,
  args?: unknown[]
): any {
  try {
    return args ? fn(...args) : fn();
  } catch (err) {
    handleError(err, instance, type);
  }
}

callWithAsyncErrorHandling

用于处理异步函数和 Promise 的错误:

export function callWithAsyncErrorHandling(
  fn: Function | Function[],
  instance: ComponentInternalInstance | null,
  type: ErrorTypes,
  args?: unknown[]
): any {
  if (isFunction(fn)) {
    const res = callWithErrorHandling(fn, instance, type, args);
    if (res && isPromise(res)) {
      res.catch((err) => {
        handleError(err, instance, type);
      });
    }
    return res;
  }
  // ... 处理函数数组的情况
}

源码实现

// packages/runtime-core/src/errorHandling.ts
/**
 * 处理组件执行过程中产生的错误
 *
 * Vue 的错误处理机制遵循以下优先级顺序:
 * 1. 组件树向上传播:从当前组件开始,向上遍历父组件链,调用 errorCaptured 钩子
 * 2. 应用级错误处理器:如果配置了 errorHandler,则调用它
 * 3. 最终日志记录:如果以上都未处理,则记录错误日志
 */
export function handleError(
  err: unknown,
  instance: ComponentInternalInstance | null | undefined,
  type: ErrorTypes,
  throwInDev = true
): void {
  // 获取错误发生的上下文 VNode(用于错误日志定位)
  const contextVNode = instance ? instance.vnode : null;

  // 从应用配置中获取错误处理器和是否在生产环境抛出错误的配置
  const { errorHandler, throwUnhandledErrorInProduction } =
    (instance && instance.appContext.config) || EMPTY_OBJ;

  // 如果有组件实例,尝试通过组件树向上传播错误
  if (instance) {
    // 从当前组件的父组件开始向上遍历
    let cur = instance.parent;

    // exposedInstance 是组件的渲染代理对象,用于保持与 Vue 2.x 的一致性
    // 在 errorCaptured 钩子中,可以通过这个对象访问组件的属性和方法
    const exposedInstance = instance.proxy;

    // ...

    // 向上遍历组件树,查找并调用 errorCaptured 钩子
    while (cur) {
      // cur.ec 是当前组件的 errorCaptured 钩子数组
      const errorCapturedHooks = cur.ec;
      if (errorCapturedHooks) {
        // 遍历所有 errorCaptured 钩子并依次调用
        for (let i = 0; i < errorCapturedHooks.length; i++) {
          // 如果钩子返回 false,表示错误已被处理,停止向上传播
          // Vue 2:使用 captured 捕获,Vue 3 这里是直接判断
          if (
            errorCapturedHooks[i](err, exposedInstance, errorInfo) === false
          ) {
            return;
          }
        }
      }
      // 继续向上查找父组件
      cur = cur.parent;
    }

    // 如果组件树中没有 errorCaptured 钩子处理错误,则使用全局错误处理 errorHandler
    if (errorHandler) {
      // 暂停响应式追踪,避免在错误处理过程中触发额外的副作用
      pauseTracking();
      // 调用应用级错误处理器
      // 注意:这里传入 null 作为 instance,因为这是全局处理,不属于特定组件
      callWithErrorHandling(errorHandler, null, ErrorCodes.APP_ERROR_HANDLER, [
        err,
        exposedInstance,
        errorInfo,
      ]);
      // 恢复响应式追踪
      resetTracking();
      // 全局已处理错误,直接返回
      return;
    }
  }

  // 如果错误没有被任何 errorCaptured 钩子或应用级处理器处理,
  // 则记录错误日志(在开发环境可能会抛出错误,生产环境只打印到控制台)
  logError(
    err,
    type,
    contextVNode,
    throwInDev,
    throwUnhandledErrorInProduction
  );
}

function logError(
  err: unknown,
  type: ErrorTypes,
  contextVNode: VNode | null,
  throwInDev = true,
  throwInProd = false
) {
  if (__DEV__) {
    const info = ErrorTypeStrings[type];
    if (contextVNode) {
      pushWarningContext(contextVNode);
    }
    warn(`Unhandled error${info ? ` during execution of ${info}` : ``}`);
    if (contextVNode) {
      popWarningContext();
    }
    // 开发环境默认抛出错误,便于调试
    if (throwInDev) {
      throw err;
    } else if (!__TEST__) {
      console.error(err);
    }
  } else if (throwInProd) {
    throw err;
  } else {
    // 生产环境恢复执行,减少对最终用户的影响
    console.error(err);
  }
}

思考

1. 为什么是父组件去捕获子组件错误,不是子组件自身?

看 Vue 源码实现,可以发现触发错误钩子都是先获取 parent 实例,然后再逐层向上查找错误钩子,而不是获取组件自身的钩子。

主要原因是子组件在出错时,组件状态可能已经不可靠。如果这时候有依赖 UI 渲染的场景,组件即使捕获了错误,也没法做 UI 降级的处理机制。

如果是在稳定的父组件中捕获错误,可以由父组件去兜底组件异常问题的处理流程。

总结

通过 errorCaptured/onErrorCaptured 钩子和全局 errorHandler 处理组件树中的错误。

采用向上冒泡的传播机制,优先级为:组件树向上传播 → 全局错误处理 → 最终 console.error

同时 Vue 3 和 Vue 2 的差异:

  • API 差异:Vue 2 使用选项式 API 的 errorCaptured,Vue 3 使用组合式 API 的 onErrorCaptured
  • 组件树遍历:Vue 2 使用 vm.$parent,Vue 3 使用 instance.parent
  • 钩子访问:Vue 2 使用 cur.$options.errorCaptured,Vue 3 使用 cur.ec(更简洁)。
  • 暴露实例:Vue 2 直接传递组件实例 vm,Vue 3 传递渲染代理 instance.proxy
  • 错误信息:Vue 2 使用固定字符串,Vue 3 开发环境用字符串,生产环境用文档链接。
  • 响应式控制:Vue 2 使用 pushTarget/popTarget,Vue 3 使用 pauseTracking/resetTracking
  • 类型支持:Vue 3 有更好的 TypeScript 类型支持,错误类型使用枚举。
  • 全局配置:Vue 2 使用 Vue.config.errorHandler,Vue 3 使用 app.config.errorHandler

参考内容

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐