When developing component libraries or plug-ins, global exception handling is often required to achieve:
- Global unified handling of exceptions;
- Prompt error messages for developers;
- Program downgrade processing and so on.
So how to achieve the above function?
This article first briefly implements an exception handling method, then combines the implementation in the Vue3 source code in detail, and finally summarizes several cores for implementing exception handling.
The Vue3 version of this article is 3.0.11
1. Common exceptions in the front end
For the front end, there are many common exceptions, such as:
- JS syntax exception;
- Ajax request exception;
- Static resource loading exception;
- Promise exception;
- iframe exception;
- etc
For how to handle these exceptions, you can read these two articles:
The most common ones are:
1. window.onerror
According to window.onerror
document , when JS runs an error (including syntax error), trigger window.onerror()
:
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕获到异常:',{message, source, lineno, colno, error});
}
Function parameters:
- message: Error message (string). Available for 06224e19ae0726 in the HTML
onerror=""
event
. - source: URL of the script where the error occurred (string)
- lineno: the line number (number) where the error occurred
- colno: the column number (numeric) where the error occurred
- error: Error object (object)
If this function returns true, execution of the default event handler is prevented.
2. try...catch exception handling
In addition, we often use the try...catch
statement to handle exceptions:
try {
// do something
} catch (error) {
console.error(error);
}
For more processing methods, you can read the articles recommended above.
3. Think
You can think about it, do you often have to deal with these error situations in the process of business development?
So complex libraries like Vue3 also handle exceptions through try...catch
everywhere?
Let's see together.
2. Implement simple global exception handling
When developing a plug-in or library, we can encapsulate a global exception handling method through try...catch
, and pass the method to be executed as a parameter. The caller only needs to care about the call result 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;
}
have a test:
const f1 = () => {
console.log('[f1 running]')
throw new Error('[f1 error!]')
}
errorHandling(f1);
/*
输出:
[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
*/
As you can see, when you need to do exception handling for a method, you only need to pass in the method as a parameter.
However, the above example is a bit different from the logic of actual business development. In actual business, we often encounter nested calls of 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)
/*
输出:
[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)
*/
This is also fine. Then the next step is to implement the corresponding exception handling in the catch
branch of the errorHandling
method.
Next, let's see 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 exception handling is implemented in the Vue3 source code, which is also very simple to implement.
1. Implement exception handling methods
Two methods for handling global exceptions, callWithErrorHandling
and callWithAsyncErrorHandling
, are defined in the errorHandling.ts
file.
As the name suggests, these two methods handle separately:
callWithErrorHandling
: Handle exceptions from synchronized methods;callWithAsyncErrorHandling
: Handle exceptions for async methods.
It is used as follows:
callWithAsyncErrorHandling(
handler,
instance,
ErrorCodes.COMPONENT_EVENT_HANDLER,
args
)
The code implementation is roughly as follows:
// packages/runtime-core/src/errorHandling.ts
// 处理同步方法的异常
export function callWithErrorHandling(
fn: Function,
instance: ComponentInternalInstance | null,
type: ErrorTypes,
args?: unknown[]
) {
let res
try {
res = args ? fn(...args) : fn(); // 调用原方法
} catch (err) {
handleError(err, instance, type)
}
return res
}
// 处理异步方法的异常
export function callWithAsyncErrorHandling(
fn: Function | Function[],
instance: ComponentInternalInstance | null,
type: ErrorTypes,
args?: unknown[]
): any[] {
// 省略其他代码
const res = callWithErrorHandling(fn, instance, type, args)
if (res && isPromise(res)) {
res.catch(err => {
handleError(err, instance, type)
})
}
// 省略其他代码
}
The logic of the callWithErrorHandling
method is relatively simple, and a layer of encapsulation is done through a simple try...catch
.
The callWithAsyncErrorHandling
method is more ingenious, by passing the method to be executed into the callWithErrorHandling
method for processing, and processing the result through the .catch
method.
2. Handling exceptions
In the above code, if an error is encountered, the exception will be handled through handleError()
. Its implementation is roughly as follows:
// packages/runtime-core/src/errorHandling.ts
// 异常处理方法
export function handleError(
err: unknown,
instance: ComponentInternalInstance | null,
type: ErrorTypes,
throwInDev = true
) {
// 省略其他代码
logError(err, type, contextVNode, throwInDev)
}
function logError(
err: unknown,
type: ErrorTypes,
contextVNode: VNode | null,
throwInDev = true
) {
// 省略其他代码
console.error(err)
}
After retaining the core processing logic, you can see that the processing here is also quite simple, and the error content is output directly through console.error(err)
.
3. Configure errorHandler custom exception handler
When using Vue3, is also supported to specify a custom exception handler to handle uncaught thrown by component rendering functions and during the execution of listeners. When this handler is called, the error message and corresponding application instance can be obtained.
Document reference: " errorHandler "
The usage method is as follows, configured in the project main.js
file:
// src/main.js
app.config.errorHandler = (err, vm, info) => {
// 处理错误
// `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子
}
So when was errorHandler()
executed? Let's continue to look at the content of handleError()
in the source code, we can find:
// 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) {
// 省略其他代码
// 读取 errorHandler 配置项
const appErrorHandler = instance.appContext.config.errorHandler
if (appErrorHandler) {
callWithErrorHandling(
appErrorHandler,
null,
ErrorCodes.APP_ERROR_HANDLER,
[err, exposedInstance, errorInfo]
)
return
}
}
logError(err, type, contextVNode, throwInDev)
}
The custom error handling function of the global configuration is obtained through instance.appContext.config.errorHandler
, and it is executed when it exists. Of course, this is also called by the previously defined callWithErrorHandling
.
4. Call the errorCaptured lifecycle hook
When using , it is also possible to catch errors from descendant components via the errorCaptured
lifecycle .
Document reference: " errorCaptured "
The parameters are as follows:
(err: Error, instance: Component, info: string) => ?boolean
This 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 to stop the error from propagating further up.
Interested students can view the specific error propagation rules
The usage method is as follows, the parent component listens onErrorCaptured
life cycle (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>
The subcomponents are as follows:
<template>
<button @click="sendMessage">发送消息</button>
</template>
<script setup>
// Message.vue
const sendMessage = () => {
throw new Error('[test onErrorCaptured]')
}
</script>
When the "Send Message" button is clicked, the console outputs 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
It can be seen that the onErrorCaptured
life cycle hook is executed normally, and the exception in the subcomponent Message.vue
is output.
So how is this achieved? Or look at the errorHandling.ts
method in handleError()
:
// 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 生命周期方法
if (errorCapturedHooks) {
// ②循环执行 errorCaptured 中的每个 Hook
for (let i = 0; i < errorCapturedHooks.length; i++) {
if (
errorCapturedHooks[i](err, exposedInstance, errorInfo "i") === false
) {
return
}
}
}
cur = cur.parent
}
// 省略其他代码
}
logError(err, type, contextVNode, throwInDev)
}
Here, we will first obtain instance.parent
as the currently processed component instance for recursion, and each time we will take out the array of errorCaptured
lifecycle methods configured by the component and call each of its hooks in a loop, then take out the parent component of the current component as a parameter, and finally continue the recursion call down.
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 conditions, so that we can easily locate the place where the exception occurs.
The error codes and error messages are as follows:
// packages/runtime-core/src/errorHandling.ts
export const enum ErrorCodes {
SETUP_FUNCTION,
RENDER_FUNCTION,
WATCH_GETTER,
WATCH_CALLBACK,
// ... 省略其他
}
export const ErrorTypeStrings: Record<number | string, string> = {
// 省略其他
[LifecycleHooks.RENDER_TRACKED]: 'renderTracked hook',
[LifecycleHooks.RENDER_TRIGGERED]: 'renderTriggered hook',
[ErrorCodes.SETUP_FUNCTION]: 'setup function',
[ErrorCodes.RENDER_FUNCTION]: 'render function',
// 省略其他
[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 different error conditions, according to the error code ErrorCodes
to get the ErrorTypeStrings
error message 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}` : ``}`)
// 省略其他
} else {
console.error(err)
}
}
6. Implement Tree Shaking
For the introduction of Vue3's implementation of Tree Shaking, you can see Efficient Implementation Framework and JS Library Slimming I wrote earlier.
Among them, the logError
method is used:
// packages/runtime-core/src/errorHandling.ts
function logError(
err: unknown,
type: ErrorTypes,
contextVNode: VNode | null,
throwInDev = true
) {
if (__DEV__) {
// 省略其他
} else {
console.error(err)
}
}
When compiled into a production environment, the code of the __DEV__
branch will not be packaged, thereby optimizing the size of the package.
4. Summary
In the above part, we have 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 code and service error information;
- Support custom error handling methods;
- Support development environment error prompts;
- Support Tree Shaking.
These points can be taken into account when you design a plug-in~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。