When developing component libraries or plug-ins, global exception handling is often required to achieve:

  • Unified handling of exceptions globally;
  • Error messages for developers
  • Scheme degradation processing and so on.

So how to implement the above function? This article first simple implementation of an exception processing method, and then combined with Vue3 source code implementation in detail, finally summed up the implementation of several core exception processing.

The Vue3 version of this article is 3.0.11

1. Common front-end anomalies

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

  • JS syntax exception;
  • Ajax request exception;
  • Static resource loading is abnormal.
  • Promise the exception;
  • The iframe exception;
  • , etc.

To learn how to handle these exceptions, read these two articles:

  • Front-end Exception Handling you Didn’t Know about
  • How to Gracefully Handle Front-end Exceptions?

The most common ones are:

1. window.onerror

The window.onerror documentation shows that window.onerror() is raised when errors (including syntax errors) occur while JS is running:

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

Function parameters:

  • Message: error message (string). Can be used for HTMLonerror=""In the handlerevent.
  • Source: script URL (string) where the error occurred
  • Lineno: Line number (number) where the error occurred
  • Colno: Column number (digit) where the error occurred
  • Error: Error object (object)

If this function returns true, it prevents execution of the default event handler.

2. try… Catch exception handling

Also, we often use try… The catch statement handles exceptions:

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

For more tips on how to deal with it, read the recommended article above.

3. Think about

Do you often deal with these errors in your business development process? Does a library as complex as Vue3 also pass tries everywhere? Catch to handle exceptions? Let’s see.

Implement simple global exception handling

When developing plug-ins or libraries, try… A catch encapsulates a global exception handling method that passes in the method to be executed as an argument, and the caller cares about the result of the call without knowing the internal logic of the global exception handling method. The general usage is as follows:

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

Test it out:

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.
      
        (/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 */
      
Copy the code

As you can see, when you need to do exception handling for a method, you simply pass in the method as a parameter. But the above example is far from the logic of real business development. In real business, we often encounter nested calls to methods, so 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.
      
        (/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) */
      
Copy the code

That’s ok. The next step is to implement exception handling in the Catch branch of the errorHandling method. How does Vue3 source code deal with this?

How does Vue3 implement exception handling

Having understood the examples above, let’s take a look at how exception handling is implemented in Vue3 source code, which is also very simple to implement.

1. Implement the exception handling method

In the errorHandling. Ts file defines callWithErrorHandling and callWithAsyncErrorHandling two global exception handling methods. As the name implies, the two methods are handled separately:

  • callWithErrorHandling: Handle synchronization method exceptions;
  • callWithAsyncErrorHandling: Handles exceptions for asynchronous methods.

The usage is as follows:

callWithAsyncErrorHandling(
  handler,
  instance,
  ErrorCodes.COMPONENT_EVENT_HANDLER,
  args
)
Copy the code

The code implementation is roughly as follows:

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

// Handle synchronization method exceptions
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
}

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

The callWithErrorHandling method handles relatively simple logic, with a simple try… Catch does a layer of encapsulation. And callWithAsyncErrorHandling method is more clever, by will need to perform incoming callWithErrorHandling treatments, and by their results. Catch method for processing.

2. Handle exceptions

In the above code, the exception is handled through handleError() whenever an error is reported. Its implementation is roughly 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 code
  logError(err, type, contextVNode, throwInDev)
}

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

Once the core processing logic is left in place, you can see that it is fairly easy to handle the error directly through console.error(err).

3. Configure the user-defined exception handling function errorHandler

With Vue3, it is also possible to specify custom exception handlers to handle uncaught errors thrown during component rendering functions and listener execution. When this handler is called, it gets the error message and the corresponding application instance. You can use errorHandler as follows and configure it 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 lifecycle hook where the error occurred
}
Copy the code

So when is errorHandler() executed? If we look at the source handleError(), we can see:

// 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 code
    // Read the 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)
}
Copy the code

Through the instance. The appContext. Config. The errorHandler take to custom error handling functions, global configuration is performed when there are, of course, this is through the previously defined callWithErrorHandling to invoke.

4. Call the errorCaptured lifecycle hook

With Vue3, errorCaptured lifecycle hooks can also be used to catch errors from descendant components. ErrorCaptured has the following parameters:

(err: Error.instance: Component, info: string) => ? booleanCopy the code

The hook receives three parameters: the error object, the component instance where the error occurred, and a string containing information about the source of the error. This hook can return false to prevent further propagation of the error. Interested students can refer to the documentation to see the specific error propagation rules. The parent listens for onErrorCaptured lifecycle (the sample code uses the 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>
Copy the code

The sub-components are as follows:

<template> < button@click ="sendMessage"> </button> </template> <script setup> // message. vue const sendMessage = () => { throw new Error('[test onErrorCaptured]') } </script>Copy the code

When the “Send message” button is clicked, the console prints 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 handlerCopy the code

You can see that the onErrorCaptured lifecycle hook is executing properly and outputs exceptions in its child component message.vue.

So how does this work? Look again at the handleError() method in errorHandling.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 ErrorCaptured lifecycle method (
      if (errorCapturedHooks) {
        // Loop through each Hook that errorCaptured
        for (let i = 0; i < errorCapturedHooks.length; i++) {
          if (
            errorCapturedHooks[i](err, exposedInstance, errorInfo) === false
          ) {
            return
          }
        }
      }
      cur = cur.parent
    }
    // omit other code
  }
  logError(err, type, contextVNode, throwInDev)
}

Copy the code

Instance.parent is retrieved recursively as the component instance it is dealing with, and an array of errorCaptured life-cycle methods configured for the component is retrieved each time and each hook is called, then the parent component is retrieved as an argument and recursively called.

5. Implement error codes and error messages

Vue3 also defines error codes and error messages for exceptions. Different error cases have different error codes and error messages, making it easy to locate the exception. The error code and error message are as follows:

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

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

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'
}
Copy the code

When there are different error situations, the ErrorTypeStrings error information can be obtained according to the ErrorCodes for prompting:

// 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)
  }
}
Copy the code

6. Tree Shaking

For an introduction to Vue3 implementing Tree Shaking, see the efficient implementation framework and JS library streamlining I wrote about earlier. The logError method is used:

// 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)
  }
}
Copy the code

When compiled into the production environment, the __DEV__ branch code is not packaged in, optimizing the package size.

Four,

In the previous section, we almost figured out the core logic of global exception handling in Vue3. We can also consider these core points when developing our own error handling methods:

  1. Support synchronous and asynchronous exception handling;
  2. Set service error codes and service error information.
  3. Support custom error handling methods;
  4. Support development environment error message;
  5. Support for Tree Shaking.