One sunny day, the interviewer met a young man. When introducing the project, the guy said that he had made an error reporting mechanism, and Vue’s error capture was used in the front end. At this time the interviewer glanced at the resume, a line of “familiar with Vue2 source code” hit the eye. After the introduction of the boy, the interviewer said that good good, then you talk about Vue’s error handling. Young man eyes a stare, wish this old iron not according to common sense card, say: this problem can’t! Next ~ Interviewer: Emmm… Okay, then the next…

After the interview, the guy immediately opened the Vue source code and decided to find out…

Understand Vue error handling

1. errorHandler

First, take a look at the Vue documentation. I don’t want to say too much here, just use it and look at the print results. The code is as follows:

// main.js
Vue.config.errorHandler = function (err, vm, info) {
  console.log('Global capture err >>>', err)
  console.log('Global capture VM >>>', vm)
  console.log('Global capture info >>>', info)
}

// App.vue. created () {const obj = {}
  // Try an error in the App component's Created hook directly, calling an fn that does not exist in obj
  obj.fn()
},
methods: {
  handleClick () {
    // Bind a click event that fires when clicked
    const obj = {}
    obj.fn()
  }
}
...
Copy the code
  1. createdThe output is as follows (this will be done at the end of the articlecatchProcess analysis) :

  1. handleClickThe output is as follows (this will be done at the end of the articlecatchProcess analysis of

Thus it can be seen that:

  • errObtain error information and stack information
  • vmGet the VM instance that reported the error (that is, the corresponding component)
  • infoSpecific error messages can be obtained. Such as life cycle informationcreated hook, event informationv-on handler

2. errorCaptured

As always, you can look at the introduction of the Vue documentation first, and also put the use case directly here. The code is as follows:

// App.vue
<template>
  // Reference the child HelloWorld in the template
  <HelloWorld />
</template>
...
errorCaptured(err, vm, info) {
  // Add errorCaptured hooks, as in the previous example
  console.log('Parent component catches err >>>', err, vm, info)
}
...

/ / HelloWorld components. created () {const child = {}
  // Create directly throws an error in the child component to see how it prints
  child.fn()
}
...
Copy the code

The following output is displayed:As you can see,HelloWorldAn error in a component goes to the App componenterrorCapturedCapture, also given globallyerrorHandlerCaptured. Isn’t it a little bit like what happened in our caseThe bubbling?

It’s important to note that errorCaptured is called when catching an error from a descendant component, meaning that it cannot capture its own. To verify this, add errorCaptured hooks to HelloWorld and print “errorCaptured” in Created.

.created() {
  console.log('Child components also catch errors with errorCaptured')
  const child = {}
  // Create directly throws an error in the child component to see how it prints
  child.fn()
},
errorCaptured(err, vm, info) {
  console.log('Subcomponent capture', err, vm, info)
}
...
Copy the code

Except for printing one more linecreated, and nothing else changes.


3. A diagram summarizes the Vue error catching mechanism


Two, Vue error capture source code

The Vue version of the source code analysis is v2.6.14 and the code is located at SRC /core/util/error.js. There are four methods: handleError, invokeWithErrorHandling, globalHandleError, and logError. Let’s look at them one by one

1. handleError

The unified error handling function in Vue, which notifying errorHandler up to global errorHandler. The core interpretation is as follows:

  • parametererr,vm,info
  • pushTarget,popTarget. Note in the source code is written, mainly to avoid handling errorscomponentInfinite rendering
  • $parent. Attributes in the Vue component tree to establish parent-child relationships, which can be used to continuously look up the top-level component —The Vue(the one we initialized with new Vue),The Vuethe$parentundefined
  • To obtainerrorCaptured. Some of you may wonder why it is hereAn array ofBecause the Vue initialization will do to the hookMerging processing. Let’s say we usemixinsThe component may have multiple identical hooks, which will be initializedcbmergeIn an array of hooks to be called when the hooks are triggered
  • capture. If it is false, return will not go toglobalHandleError

The source code is as follows:

// Err, vm, info, etc
function handleError (err: Error, vm: any, info: string) {
  pushTarget()
  try {
    if (vm) {
      let cur = vm
      // Look up $parent until it doesn't exist
      // Pay attention! Cur is assigned to cur.$parent at first, so errorCaptured is not executed in error capture for the current component
      while ((cur = cur.$parent)) {
        // Get hooks errorCaptured
        const hooks = cur.$options.errorCaptured
        if (hooks) {
          for (let i = 0; i < hooks.length; i++) {
            try {
              / / errorCaptured execution
              const capture = hooks[i].call(cur, err, vm, info) === false
              ErrorCaptured returns false, and the outer globalHandleError is not executed
              if (capture) return
            } catch (e) {
              // If errorCaptured is captured, globalHandleError is executed, and errorCaptured hook is info
              globalHandleError(e, cur, 'errorCaptured hook')}}}}}// Global capture, as long as the above does not return, will be executed
    globalHandleError(err, vm, info)
  } finally {
    popTarget()
  }
}
Copy the code

2. invokeWithErrorHandling

A wrapper function that internally wraps the passed function with a try-catch and has better handling of asynchronous errors. Handles error catching for lifecycle, event, and other callback functions. An asynchronous error capture with a processable return value of Promise. When an error is caught, it is sent to handleError, which handles the logic for notification up to the global level. The core interpretation is as follows:

  • parameterhandler. The incoming execution function, called internally, makes a Promise on its return value
  • try-catch. Use try-catch to wrap and execute the function passed in, and call it when an error is caughthandleError. (Isn’t it a little bitHigher-order functionsWhat about the smell?)
  • handleError. After catching an error, the handleError method is also called to notify the error upward
function invokeWithErrorHandling (
  handler: Function,
  context: any,
  args: null | any[],
  vm: any,
  info: string
) {
  let res
  try {
    // Handle the arguments of handle and call
    res = args ? handler.apply(context, args) : handler.call(context)
    // Check if the return is a Promise and no catch(! res._handled)
    if(res && ! res._isVue && isPromise(res) && ! res._handled) { res.catch(e= > handleError(e, vm, info + ` (Promise/async)`))
      // _handled flag is set to true to avoid multiple catches when nested calls are handled
      res._handled = true}}catch (e) {
    // Call handleError when an error is caught
    handleError(e, vm, info)
  }
  return res
}
Copy the code

3. globalHandleError

Global error capture. This is the triggering function of vue.config. errorHandler that we configured globally

  • Internal usetry-catchThe parcelerrorHandlerThe execution. This is where our global error-catching function ~ is executed
  • If you executeerrorHandlerinerrorIs captured after passinglogErrorPrinting. (logErrorUse in the browser production environmentconsole.errorPrinting)
  • If there is noerrorHandler. It will be used directlylogErrorError print
function globalHandleError (err, vm, info) {
  if (config.errorHandler) {
    try {
      // Call global errorHandler and return
      return config.errorHandler.call(null, err, vm, info)
    } catch (e) {
      If the user intentionally throws an original error in a handler, do not log it twice
      if(e ! == err) {// Catch errors in globalHandleError, output by logError
        logError(e, null.'config.errorHandler')}}}// If errorHandler is not captured globally, execute here with logError error
  logError(err, vm, info)
}
Copy the code

4. logError

Print error messages (development environment, online will vary)

  • warn. Warn is used in the development environment to print errors. In order to[Vue warn]:At the beginning
  • console.error. Used in a browser environmentconsole.errorOutput any errors caught
// logError source code implementation
function logError (err, vm, info) {
  if(process.env.NODE_ENV ! = ='production') {
    Warn is used in the development environment to output errors
    warn(`Error in ${info}:"${err.toString()}"`, vm)
  }
  /* istanbul ignore else */
  if ((inBrowser || inWeex) && typeof console! = ='undefined') {
    // Directly print the error information with console.error
    console.error(err)
  } else {
    throw err
  }
}

// Take a quick look at the warn implementation
warn = (msg, vm) = > {
  const trace = vm ? generateComponentTrace(vm) : ' '
  if (config.warnHandler) {
    config.warnHandler.call(null, msg, vm, trace)
  } else if(hasConsole && (! config.silent)) {// This is where Vue warn usually prints errors!
    console.error(`[Vue warn]: ${msg}${trace}`)}}Copy the code

Looking at the figure below, does the error output in the development environment look familiar if we don’t do global error catching? 👇

  • Here’s a quick question: Why does one error print two error messages?

This is an implementation of the logError function. As a reminder, logError calls WARN to print a Vue wrapped error message starting with [Vue WARN]:, and then prints the JS error message through console.error

A quick summary:

  1. handleError: unified error trapping function. Implement sub-component to top-level component error capture post pairerrorCapturedA bubbling call to a hook that executes a fullerrorCapturedThe global error-catching function globalHandleError is finally executed after the hook.
  2. invokeWithErrorHandling: wrapping function, passHigher-order functionsThe private idea of programming by receiving a function argument and using it internallytry-catchAfter the parcelperformFunction passed in; They offer better onesAsynchronous error handlingWhen an executing function returns a Promise object, its implementation is caught and notifiedhandleError(if we don’t catch the returned Promise ourselves)
  3. globalHandleErrorErrorHandler: Calls the errorHandler function of the global configuration. If an error is caught during the call, it passeslogErrorPrints the errors caught, ending with ‘config.errorHandler’
  4. logError. Implements the printing of uncaptured error information. The development environment printsTwo kinds ofError message ~

Error capture process analysis

After looking at the source code implementation of error capture, it is better to take a look at how Vue catch errors, in order to deepen the understanding. There are many ways to catch a hit error. Here is the code case at the beginning of the article as a hit branch for debugging, take you to see how Vue is to achieve error capture ~

1. createdPhase error capture

Review what the entire componentization process (lifecycle) of Vue does, as shown below:

The trigger phase for created is in the init phase, as shown below:As you can see, the created hook is triggered bycallHookMethod, let’s seecallHookThe implementation of the:

  • Traverses the current VM instanceThe currentHook all cb and pass it ininvokeWithErrorHandlingIn the function
  • invokeWithErrorHandlingCb will be called, which will catch an error and executehandleError. And now it’s in the App component, and then it goes upThe VueAnd already in useerrorHandlerGlobal error catching, so a series of console.log “global catches” are triggered.
function callHook (vm, hook) {
  pushTarget();
  var handlers = vm.$options[hook];
  // Info, this is a created hook
  var info = hook + " hook";
  if (handlers) {
    for (var i = 0, j = handlers.length; i < j; i++) {
      // Call invokeWithErrorHandling directly, passing in the corresponding CB
      invokeWithErrorHandling(handlers[i], vm, null, vm, info); }}if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook);
  }
  popTarget();
}
Copy the code

2. Error capture of click events

The example code is the same as the click for errorHandler in Vue error handling, but there is only one more line of console.log, so that you can see the packaged code for deeper understanding. This section refers to another point in the Vue source code — events. Of course, I’m not going to expand it out here, but you get the idea. I will write another chapter on Vue event source parsing ~

// Template code
<template>
  <div id="app">
    <button @click="handleClick">click</button>
  </div>
</template>

/ / js code
methods: {
  handleClick () {
    console.log('Click event error capture')
    const obj = {}
    obj.fn()
  }
}
Copy the code

The packaged code looks like this:Thus, during the entire Vue initialization process, the click event that we bound is performedupdateDOMListenersIs then called toupdateListenersThis method, let’s look at itupdateListenersWhat does the core code do?Here we do not have to go into the reason ha!! Just know the order in which this process is called, because post it so you can understand it a little more clearly. If you are interested in the author can wait for a Vue event source analysis ha ~

function updateListeners () {
  // Cur is handleClick
  cur = on[name] = createFnInvoker(cur, vm);
}
Copy the code

As you can see, our handleClick is wrapped and returned with createFnInvoker, where our error capture is implemented. So let’s see what createFnInvoker does, right

function createFnInvoker (fns, vm) {
  function invoker () {
    var arguments$1 = arguments;
    // Get the method from invoker's static property FNS
    var fns = invoker.fns;
    if (Array.isArray(fns)) {
      // A new array of FNS
      var cloned = fns.slice();
      for (var i = 0; i < cloned.length; i++) {
        // Wrap FNS with invokeWithErrorHandling
        invokeWithErrorHandling(cloned[i], null, arguments$1, vm, "v-on handler"); }}else {
      // The same is true here, except that the single FNS is wrapped with invokeWithErrorHandling
      return invokeWithErrorHandling(fns, null.arguments, vm, "v-on handler")}}// In this case, FNS, which is cur, is our handleClick method
  invoker.fns = fns;
  // Returns an invoker, which we clicked to trigger
  return invoker
}
Copy the code

To sum up:

  • Every time we click, the surface is triggeredhandleClick“Is actually triggering a decoratorinvoker
  • Again byinvokerTo callinvokeWithErrorHandlingAnd pass in the saved in invokerStatic attributesFunctions in FNS (that is, we users writehandleClickFunction)
  • In this case, it’s likeTwo, Vue error capture source codeIn the2. invokeWithErrorHandlingThe execution is the same
  • Will eventually passhandleErrorImplement bubbling up the error hook of the upper component until global error capture

This is our error capture process for click events


How about at the end? Isn’t that easy? Error catching is important both at the framework level and in our daily development business, but often overlooked by many (like me). Overview down, in fact, this piece is not difficult, in the implementation of Vue source code, as long as we can understand. Anyway ~ learn a bit more not bad, the interview asked to don’t panic, though not the core focus of Vue interview, but pluses, can ask to answer must be, that if no answer at all, it could reduce it, especially in the project wrote a Vue error trapping related to, after all, this is a lot easier than reactive the ha ha ha ~