How does Vue3 implement global exception handling?

Posted by adsegzy on Sun, 06 Mar 2022 15:05:17 +0100

The plug-in library often needs to be developed globally, or exception handling is required:

  • Handle exceptions globally;
  • Prompt error messages for developers;
  • Scheme degradation processing, etc.

So how to realize the above functions?
This paper first briefly implements an exception handling method, then introduces it in detail combined with the implementation in Vue3 source code, and finally summarizes several cores of exception handling.

The Vue3 version of this article is 3.0.11

1, Common front-end exceptions

For the front end, there are many common exceptions, such as:

  • JS syntax exception;
  • Ajax request exception;
  • Abnormal loading of static resources;
  • Promise exception;
  • iframe exception;
  • wait

For how to handle these exceptions, you can read these two articles:

The most commonly used are:

1. window.onerror

Through window onerror file It can be seen that when JS runs with errors (including syntax errors), it triggers window onerror():

window.onerror = function(message, source, lineno, colno, error) {
  console.log('Exception caught:',{message, source, lineno, colno, error});
}

Function parameters:

  • Message: error message (string). Can be used for event s in the HTML oneror = "" handler.
  • source: script URL (string) where the error occurred
  • lineno: the line number (number) where the error occurred
  • colno: the column number (number) where the error occurred
  • error: Error object (object)

If the function returns true, the default event handler function is prevented from executing.

2. try...catch exception handling

In addition, we often use try catch sentence Exception handling:

try {
  // do something
} catch (error) {
  console.error(error);
}

For more processing methods, you can read the previously recommended articles.

3. Thinking

You can think about whether you often have to deal with these errors in the process of business development?
So, does a complex library like Vue3 also try everywhere Catch to handle exceptions?
Let's take a look.

2, Implement simple global exception handling

When developing plug-ins or libraries, we can try Catch encapsulates a global exception handling method and passes in the method to be executed as a parameter. The caller only cares about the call result without knowing the internal logic of the global exception handling method.
The general method of use is as follows:

const errorHandling = (fn, args) => {
  let result;
  try{
    result = args ? fn(...args) : fn();
  } catch (error){
    console.error(error)
  }
  return result;
}

Test:

const f1 = () => {
    console.log('[f1 running]')
    throw new Error('[f1 error!]')
}

errorHandling(f1);
/*
 Output:
 [f1 running]
Error: [f1 error!]
    at f1 (/Users/wangpingan/leo/www/node/www/a.js:14:11)
    at errorHandling (/Users/wangpingan/leo/www/node/www/a.js:4:39)
    at Object.<anonymous> (/Users/wangpingan/leo/www/node/www/a.js:17:1)
    at Module._compile (node:internal/modules/cjs/loader:1095:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1147:10)
    at Module.load (node:internal/modules/cjs/loader:975:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
    at node:internal/main/run_main_module:17:47
*/

You can see that when you need to do exception handling for a method, just pass the method as a parameter.
However, the logic of the above example is a little different from that of the actual business development. In the actual business, we often encounter nested calls of methods. Let's try:

const f1 = () => {
    console.log('[f1]')
    f2();
}

const f2 = () => {
    console.log('[f2]')
    f3();
}

const f3 = () => {
    console.log('[f3]')
    throw new Error('[f3 error!]')
}

errorHandling(f1)
/*
  Output:
  [f1 running]
  [f2 running]
  [f3 running]
  Error: [f3 error!]
    at f3 (/Users/wangpingan/leo/www/node/www/a.js:24:11)
    at f2 (/Users/wangpingan/leo/www/node/www/a.js:19:5)
    at f1 (/Users/wangpingan/leo/www/node/www/a.js:14:5)
    at errorHandling (/Users/wangpingan/leo/www/node/www/a.js:4:39)
    at Object.<anonymous> (/Users/wangpingan/leo/www/node/www/a.js:27:1)
    at Module._compile (node:internal/modules/cjs/loader:1095:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1147:10)
    at Module.load (node:internal/modules/cjs/loader:975:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
*/

That's no problem. Then the next step is to implement the corresponding exception handling in the catch branch of the errorHandling method.
Next, let's take a look at how it is handled in the Vue3 source code?

3, How Vue3 implements exception handling

After understanding the above example, let's take a look at how to implement exception handling in Vue3 source code, which is also very simple to implement.

1. Implement exception handling method

In errorhandling TS file defines callWithErrorHandling and callWithAsyncErrorHandling methods to handle global exceptions.
As the name suggests, these two methods are handled separately:

  • callWithErrorHandling: handle exceptions of synchronization methods;
  • callWithAsyncErrorHandling: handles exceptions of asynchronous methods.

The usage is as follows:

callWithAsyncErrorHandling(
  handler,
  instance,
  ErrorCodes.COMPONENT_EVENT_HANDLER,
  args
)

The code implementation is roughly as follows:

// packages/runtime-core/src/errorHandling.ts

// Exception handling synchronization method
export function callWithErrorHandling(
  fn: Function,
  instance: ComponentInternalInstance | null,
  type: ErrorTypes,
  args?: unknown[]
) {
  let res
  try {
    res = args ? fn(...args) : fn(); // Call the original method
  } catch (err) {
    handleError(err, instance, type)
  }
  return res
}

// Handling exceptions of asynchronous methods
export function callWithAsyncErrorHandling(
  fn: Function | Function[],
  instance: ComponentInternalInstance | null,
  type: ErrorTypes,
  args?: unknown[]
): any[] {
  // Omit other codes
  const res = callWithErrorHandling(fn, instance, type, args)
  if (res && isPromise(res)) {
    res.catch(err => {
      handleError(err, instance, type)
    })
  }
  // Omit other codes
}

The logic of the callWithErrorHandling method is relatively simple. Through a simple try Catch makes a layer of encapsulation.
The callWithAsyncErrorHandling method is ingenious. The method to be executed is passed into the callWithErrorHandling method for processing, and the result is passed catch method.

2. Handling exceptions

In the above code, when an error is reported, the exception will be handled through handleError(). The implementation is as follows:

// packages/runtime-core/src/errorHandling.ts

// Exception handling method
export function handleError(
  err: unknown,
  instance: ComponentInternalInstance | null,
  type: ErrorTypes,
  throwInDev = true
) {
  // Omit other codes
  logError(err, type, contextVNode, throwInDev)
}

function logError(
  err: unknown,
  type: ErrorTypes,
  contextVNode: VNode | null,
  throwInDev = true
) {
  // Omit other codes
  console.error(err)
}

After retaining the core processing logic, you can see that the processing here is also quite simple, directly through the console Error (ERR) outputs the error content.

3. Configure errorHandler custom exception handling function

When using Vue3, it is also supported to specify custom exception handling functions to handle uncapped errors thrown during the execution of component rendering functions and listeners. When this processing function is called, the error information and the corresponding application instance can be obtained.
Document reference:< errorHandler>
The usage method is as follows, in the project main JS file:

// src/main.js

app.config.errorHandler = (err, vm, info) => {
  // Processing error
  // `info ` is Vue specific error information, such as the life cycle hook of the error
}

So when does errorHandler() execute? Let's continue to look at the content of handleError() in the source code. We can find that:

// packages/runtime-core/src/errorHandling.ts

export function handleError(
  err: unknown,
  instance: ComponentInternalInstance | null,
  type: ErrorTypes,
  throwInDev = true
) {
  const contextVNode = instance ? instance.vnode : null
  if (instance) {
    // Omit other codes
    // Read errorHandler configuration item
    const appErrorHandler = instance.appContext.config.errorHandler
    if (appErrorHandler) {
      callWithErrorHandling(
        appErrorHandler,
        null,
        ErrorCodes.APP_ERROR_HANDLER,
        [err, exposedInstance, errorInfo]
      )
      return
    }
  }
  logError(err, type, contextVNode, throwInDev)
}

Through instance appContext. config. Errorhandler gets the user-defined error handling function configured globally and executes it when it exists. Of course, this is also called through callWithErrorHandling defined above.

4. Call errorCaptured lifecycle hook

When using Vue3, you can also capture errors from descendant components through the errorCaptured life cycle hook.
Document reference:< errorCaptured>
The input parameters are as follows:

(err: Error, instance: Component, info: string) => ?boolean

This hook will receive three parameters: the error object, the component instance where the error occurred, and a string containing the error source information.
This hook can return false to prevent the error from continuing to propagate upward.
Interested students can view specific error propagation rules through documents.
The usage method is as follows. The parent component listens to the onErrorCaptured life cycle (the example code uses Vue3 setup syntax):

<template>
  <Message></Message>
</template>
<script setup>
// App.vue  
import { onErrorCaptured } from 'vue';
  
import Message from './components/Message.vue'
  
onErrorCaptured(function(err, instance, info){
  console.log('[errorCaptured]', err, instance, info)
})
</script>

The sub components are as follows:

<template>
  <button @click="sendMessage">send message</button>
</template>

<script setup>
// Message.vue
const sendMessage = () => {
  throw new Error('[test onErrorCaptured]')
}
</script>

When you click the "send message" button, the console will output an error:

[errorCaptured] Error: [test onErrorCaptured]
    at Proxy.sendMessage (Message.vue:36:15)
    at _createElementVNode.onClick._cache.<computed>._cache.<computed> (Message.vue:3:39)
    at callWithErrorHandling (runtime-core.esm-bundler.js:6706:22)
    at callWithAsyncErrorHandling (runtime-core.esm-bundler.js:6715:21)
    at HTMLButtonElement.invoker (runtime-dom.esm-bundler.js:350:13) Proxy {sendMessage: ƒ, ...} native event handler

You can see that the onErrorCaptured life cycle hook executes normally and outputs the sub component message Exception in Vue.

So how is this realized? Or look at errorhandling handleError() method in TS:

// packages/runtime-core/src/errorHandling.ts

export function handleError(
  err: unknown,
  instance: ComponentInternalInstance | null,
  type: ErrorTypes,
  throwInDev = true
) {
  const contextVNode = instance ? instance.vnode : null
  if (instance) {
    let cur = instance.parent
    // the exposed instance is the render proxy to keep it consistent with 2.x
    const exposedInstance = instance.proxy
    // in production the hook receives only the error code
    const errorInfo = __DEV__ ? ErrorTypeStrings[type] : type
    while (cur) {
      const errorCapturedHooks = cur.ec // ① Take out the errorCaptured life cycle method of component configuration
      if (errorCapturedHooks) {
        // ② Loop executes each Hook in errorCaptured
        for (let i = 0; i < errorCapturedHooks.length; i++) {
          if (
            errorCapturedHooks[i](err, exposedInstance, errorInfo "i") === false
          ) {
            return
          }
        }
      }
      cur = cur.parent
    }
    // Omit other codes
  }
  logError(err, type, contextVNode, throwInDev)
}

You will get instance Parent recurses as the currently processed component instance. Each time, it will take out the array of errorCaptured life cycle methods configured by the component and call each hook circularly, then take out the parent component of the current component as a parameter, and finally continue to call recursively.

5. Implement error codes and error messages

Vue3 also defines error codes and error messages for exceptions. There are different error codes and error messages in different error situations, so that we can easily locate the place where the exception occurs.
The error code and error information are as follows:

// packages/runtime-core/src/errorHandling.ts

export const enum ErrorCodes {
  SETUP_FUNCTION,
  RENDER_FUNCTION,
  WATCH_GETTER,
  WATCH_CALLBACK,
  // ...  Omit others
}

export const ErrorTypeStrings: Record<number | string, string> = {
  // Omit others
  [LifecycleHooks.RENDER_TRACKED]: 'renderTracked hook',
  [LifecycleHooks.RENDER_TRIGGERED]: 'renderTriggered hook',
  [ErrorCodes.SETUP_FUNCTION]: 'setup function',
  [ErrorCodes.RENDER_FUNCTION]: 'render function',
  // Omit others
  [ErrorCodes.SCHEDULER]:
    'scheduler flush. This is likely a Vue internals bug. ' +
    'Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/vue-next'
}

In case of different error conditions, get ErrorTypeStrings error information according to the error code ErrorCodes to prompt:

// packages/runtime-core/src/errorHandling.ts

function logError(
  err: unknown,
  type: ErrorTypes,
  contextVNode: VNode | null,
  throwInDev = true
) {
  if (__DEV__) {
    const info = ErrorTypeStrings[type]
    warn(`Unhandled error${info ? ` during execution of ${info}` : ``}`)
    // Omit others
  } else {
    console.error(err)
  }
}

6. Realize Tree Shaking

For an introduction to Vue3's implementation of Tree Shaking, see what I wrote before Efficient implementation of framework and JS library slimming.
The logError method uses:

// packages/runtime-core/src/errorHandling.ts

function logError(
  err: unknown,
  type: ErrorTypes,
  contextVNode: VNode | null,
  throwInDev = true
) {
  if (__DEV__) {
    // Omit others
  } else {
    console.error(err)
  }
}

When compiled into the production environment__ DEV__ Branching code is not packaged to optimize the size of the package.

4, Summary

From the above part, we can almost understand the core logic of global exception handling in Vue3. When developing our own error handling methods, we can also consider these core points:

  1. Support synchronous and asynchronous exception handling;
  2. Set business error code and business error information;
  3. Support user-defined error handling methods;
  4. Support development environment error prompt;
  5. Support Tree Shaking.

These points can be taken into account when designing plug-ins~

Topics: Javascript Front-end Vue.js