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 HTML
onerror=""
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:
- Support synchronous and asynchronous exception handling;
- Set service error codes and service error information.
- Support custom error handling methods;
- Support development environment error message;
- Support for Tree Shaking.