1. 为什么要异常处理
异常:程序发生了意想不到的情况,这种情况影响到了程序的正确运行。 异常的影响:当异常发生时,最好的情况是用户自己不知道发生了什么,然后再重试;最坏的情况是用户感觉特别厌烦,于是永远不回来了。有一个良好的异常处理策略可以让用户知道到底发生了什么。 举个例子:sql-editor轮询执行历史,异常未中断情况。
2. 基础概念
2.1 try/catch/finally语句
function getOwner() {
try {
// 可能出错的代码
const owner = "wentang";
owner = "other";
return "try";
} catch (error) {
// 出错时要做什么
console.log(error);
return "catch";
} finally {
// 始终执行的的代码
return "finally";
}
}
console.log(getOwner());
// TypeError: Assignment to constant variable.
// at getOwner (D:\iflyItem\demo.js:5:11)
// ...stack
// finally
- 如果
try
块中的代码运行发生错误,代码会立即退出执行,并跳到catch
块中。catch
块此时接收到一个对象,该对象包含发生错误的相关信息(通常仅使用message
属性); - 如果
try
块中的代码正常运行完,则接着执行finally
块中的代码;如果try
块中的代码运行发生错误,则执行catch
块中的代码,最后执行finally
块中的代码。try
或catch
块无法阻止finally
块执行,包括return
语句; - 如果写出
finally
字句,catch
块就成了可选的(它们两者中只有一个是必需的); - 搭配
throw
操作符,语法上来说可以抛出任何东西,而不仅仅是错误对象,但是最好始终抛出正确的错误对象,这样有助于在代码中进行错误的一致性处理,其他成员可以访问error.message
或error.stack
来知道错误的源头; try-catch
只能捕获到同步的运行时错误,对语法和异步错误无能为力,捕获不到。
2.2 错误类型
ECMA-262中定义下了下面9中错误类型:
- Error:错误的基类型,其他错误类型都继承该类型
- EvalError:关于
eval()
函数的错误,不再会被JavaScript
抛出 - InternalError:会在底层
JavaScript
引擎抛出异常时由浏览器抛出 - RangeError:会在数值越界时抛出
- ReferenceError:引用一个不存在的变量时抛出
- SyntaxError:语法错误
- TypeError:会在变量不是预期类型或者访问不存在的方法时抛出
- URIError:会在使用
encodeURI()
或decodeURI()
但传入格式错误的URI
时抛出 - AggregateError⌛:包裹了由一个操作产生且需要报告的多个错误
浏览中的中的错误还有:
- DOMException:调用方法或访问 Web API 属性时发生的异常事件
- DOMError:已经废弃,不再使用了
3. 异常分类
按照产生异常时程序是否正在运行,我们可以将异常分为编译时异常和运行时异常。 编译时异常指的是源代码在编译成可执行代码之前产生的异常,而运行时异常指的是可执行代码被装载到内存中执行之后产生的异常。
3.1 编译时异常
使用ts
时,ts
最终会编译成js
,从而在js
运行时中执行,这里的编译过程可能发生异常。
const owner: string = 10086;
// demo.ts:1:7 - error TS2322: Type 'number' is not assignable to type 'string'.
// 1 const owner: string = 10086;
// ~~~~~
// Found 1 error in demo.ts:1
同样js
也有编译时异常,常见的异常就是存在语法错误,导致编译不通过。
console.log("line 1");
await "line 2";
console.log("line 3");
// D:\mySourceOfLearning\vue2源码\vue-2.6.14\demo.js:2
// await "line 2";
// ^^^^^
// SyntaxError: await is only valid in async function
// at wrapSafe (internal/modules/cjs/loader.js:1054:16)
// at Module._compile (internal/modules/cjs/loader.js:1102:27)
// at Object.Module._extensions..js (internal/modules/cjs/loader.js:1158:10)
// at Module.load (internal/modules/cjs/loader.js:986:32)
// at Function.Module._load (internal/modules/cjs/loader.js:879:14)
// at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:71:12)
// at internal/main/run_main_module.js:17:47
通过在代码构建流程中添加静态代码分析或代码检查器,可以实现预先发现非常多的错误。常用的静态分析工具有eslint
、tslint
、stylint
、vetur
,使用TypeScript
可以提供编译时的静态类型检查。 总体来说,编译异常可以在代码被编译成可执行代码前被发现,因此对我们的伤害较小。
3.2 运行时异常
console.log("line 1");
console.log(line2);
console.log("line 3");
// line 1
// D:\mySourceOfLearning\vue2源码\vue-2.6.14\demo.js:2
// console.log(line2);
// ^
// ReferenceError: line2 is not defined
// at Object.<anonymous> (D:\mySourceOfLearning\vue2源码\vue-2.6.14\demo.js:2:13)
// at Module._compile (internal/modules/cjs/loader.js:1138:30)
// at Object.Module._extensions..js (internal/modules/cjs/loader.js:1158:10)
// at Module.load (internal/modules/cjs/loader.js:986:32)
// at Function.Module._load (internal/modules/cjs/loader.js:879:14)
// at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:71:12)
// at internal/main/run_main_module.js:17:47
如上,终端只打印出了”line 1″。这是因为异常是在运行过程中抛出的,因此这个异常属于运行时异常。相当于编译时异常,运行时异常更加难以发现。比如异常只会发生在某一个流程控制块中,例如: 这种运行时异常排查的难度就会很大了,很有可能造成“在我的电脑上好好的,到你的电脑上怎么就不行了”。
4. 异常传播及处理
4.1 异常传播
js
中异常传播是自动的,不需要我们手动地一层层传递。如果一个异常没有被 catch
,它会沿着函数调用栈一层层传播直到栈空。未被catch
的异常会导致程序抛出unCaughtError
,打印在控制台上,里面有详细的错误信息、堆栈信息。异常处理的一大目标就是避免unCaughtError
,这要求我们要明确自己写的程序可能发生哪些异常。
// lodash debounce函数
function debounce(func, wait, options) {
// ...
if (typeof func !== "function") {
throw new TypeError("Expected a function");
}
// ...
}
debounce('22', 1000)()
// Uncaught TypeError: Expected a function
// at Function.debounce (lodash.js:10385:15)
// at index.html:12:15
抛出异常时,将暂停当前函数的执行,开始查找匹配的 catch
子句。首先检查 throw
本身是否在 try
块内部,如果是,检查与该 try
相关的 catch
子句,看是否可以处理该异常。如果不能处理,就退出当前函数,并且释放当前函数的内存并销毁局部对象,继续到上层的调用函数中查找,直到找到一个可以处理该异常的 catch
。这个过程称为栈展开(stack unwinding)。当处理该异常的 catch
结束之后,紧接着该 catch
之后的代码继续执行。
4.2 同步中的异常处理
同步中的异常处理可以使用try/catch/finally
,这里不再赘述。
4.3 异步中的异常处理
js
本质上是同步的,是一种单线程语言。诸如浏览器引擎之类的宿主环境使用许多Web API,增强了 js
以与外部系统进行交互并处理与 I/O 绑定的操作。浏览器中异步操作有:定时器相关的函数、事件、Ajax请求和 Promise等。
4.3.1 定时器的错误处理
function failAfterOneSecond() {
setTimeout(() => {
throw Error("发生错误");
}, 1000);
}
try {
failAfterOneSecond();
} catch (error) {
console.error("捕获到的错误:", error.message);
}
// Error: 发生错误
// at Timeout._onTimeout (D:\iflyItem\demo.js:3:15)
// ...stack
可以看出来try/catch
没有捕获到异常,这是因为try/catch
是同步,而setTimeout
是异步的。当执行到setTimeout
回调时,函数调用栈早就没有try/catch
的踪影了,所以异常就无法捕获到。
4.3.2 使用Promise处理异常
// Promise处理定时器的异常
function failAfterOneSecond() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(Error('发生错误'));
}, 1000);
});
}
failAfterOneSecond().catch(
error => console.log('捕获到的错误:', error.message) // 捕获到的错误: 发生错误
);
const promiseSuccess1 = Promise.resolve('resPromiseSuccess1');
const promiseSuccess2 = Promise.resolve('resPromiseSuccess2');
const promiseFaied1 = Promise.reject(Error('resPromiseFaied1'));
const promiseFaied2 = Promise.reject(Error('resPromiseFaied2'));
// Promise.all处理错误
Promise.all([promiseSuccess1, promiseSuccess2]).then(res => console.log(res));
// [ 'resPromiseSuccess1', 'resPromiseSuccess2' ]
// Promise.any处理错误
Promise.any([promiseFaied1, promiseFaied2]).catch(error => console.log(error));
// AggregateError: All promises were rejected
// error.errors:['resPromiseFaied1', 'resPromiseFaied2']
// Promise.race处理错误
Promise.race([promiseSuccess2, promiseSuccess1, promiseFaied2])
.then(res => console.log(res))
.catch(error => console.log(error));
// resPromiseSuccess2
// Promise.allSettled处理错误
Promise.allSettled([promiseSuccess1, promiseSuccess2, promiseFaied1, promiseFaied2])
.then(res => console.log(res))
.catch(error => console.log(error));
// [
// { status: 'fulfilled', value: 'resPromiseSuccess1' },
// { status: 'fulfilled', value: 'resPromiseSuccess2' },
// { status: 'rejected', reason: 'resPromiseFaied1' },
// { status: 'rejected', reason: 'resPromiseFaied2' }
// ];
5. 异常捕获
5.1 window.onerror
当 js
运行时错误发生时,window
会触发一个 ErrorEvent
接口的 error
事件,并执行 window.onerror()
。
window.onerror = function (message, source, lineno, colno, error) {
/*
message:错误信息 Uncaught TypeError: Assignment to constant variable.
source:发生错误的脚本URL http://127.0.0.1:5501/error.html
lineno:发生错误的行号 34
colno:发生错误的列号 11
error:Error对象
TypeError: Assignment to constant variable.
at error.html:34:11
*/
console.log("捕获到异常:", message, source, lineno, colno, error);
return true
}
可以捕获同步运行时错误、异步运行时错误,无法捕获语法错误、网络请求错误(静态资源、接口)。另外,window.onerror
函数只有在返回true
时,异常才不会向上抛出。
5.2 window.addEventListener(‘error’,erorHandler,flag)
当一项资源(如<img>
或<script>
)加载失败,加载资源的元素会触发一个Event
接口的error
事件,并执行该元素上的onerror()
处理函数,这些error
时间不会冒泡到window
。 用法:
- 第三个参数为
true
时,捕获状态可以捕获到同步运行时错误,也能捕获静态资源加载错误; - 第三次参数为
false
时,冒泡状态能捕获到同步运行时错误,不能捕获静态资源加载错误。
window.onerror
能捕获到错误的函数堆栈信息,存在error.stack
中,所以使用window.onerror
的方式对js
运行时错误进行捕获更合适些,但不是很必要。
5.3 Promise 异常
没有写 catch
的 Promise
中抛出的错误无法被 onerror
或 try-catch
捕获到,所以我们务必要在 Promise
中不要忘记写 catch
处理抛出的异常。 可以在全局增加一个对 unhandledrejection
的监听,用来全局监听Uncaught Promise Error
。
window.addEventListener("unhandledrejection", function (event) {
event.preventDefault()
// event:PromiseRejectionEvent对象 .promise属性 .reason属性
console.log('捕获到异常:', event);
return true
});
event.preventDefault()
可以用来取消控制台的Uncaught (in promise) reason
。
5.4 addGlobalUncaughtErrorHandler
addGlobalUncaughtErrorHandler
是乾坤官网提供的用来添加全局的未捕获异常处理器。 源码变更记录:
function render(props){
addGlobalUncaughtErrorHandler((event, source, lineno, colno, error) => {
console.log('捕获到异常:', event, source, lineno, colno, error);
});
}
5.5 Vue 异常
对于 Vue
的错误上报需要用其提供的 Vue.config.errorhandler
方法,但是错误一旦被这个方法捕获,就不会外抛到在控制台。
Vue.config.errorHandler = (err,vm,info) => {
// errorHandler(err: Error, vm: Vue, info: string): void
console.error('通过vue errorHandler捕获的错误');
console.error(err);
console.error(vm);
console.error(info);
}
// info到底是啥,源码整理结果如下:
// 文件 值
// error.js errorCaptured hook
// render.js render renderError 可查官网api了解更多
// state.js data() `callback for immediate watcher "${watcher.expression}"`
// watcher.js `getter for watcher "${this.expression}"`
// `callback for watcher "${this.expression}"`
// next-tick.js nextTick
// directive.js `directive ${dir.name} ${hook} hook`
// render-component-template.js @render
// lifecycle.js `${hook} hook`
// update-listeners.js `v-on handler`
// evenet.js `event handler for "${event}"`
errorCaptured
在捕获一个来自后代组件的错误时被调用,message-dialog
组件发生错误会被父组件及祖先组件的errorCaptured
捕获到。
<template>
<div>
<message-dialog></message-dialog>
</div>
</template>
<script>
export default {
errorCaptured(err, vm, info) {
// (err: Error, vm: Component, info: string) => ?boolean
console.log(err, vm, info);
}
}
</script>
错误传播规则:
- 默认情况下,如果全局的
config.errorHandler
被定义,所有的错误仍会发送它,因此这些错误仍然会向单一的分析服务的地方进行汇报; - 如果一个组件的 inheritance chain (继承链)或 parent chain (父链)中存在多个
errorCaptured
钩子,则它们将会被相同的错误逐个唤起; - 如果此
errorCaptured
钩子自身抛出了一个错误,则这个新错误和原本被捕获的错误都会发送给全局的config.errorHandler
; - 一个
errorCaptured
钩子能够返回false
以阻止错误继续向上传播。本质上是说“这个错误已经被搞定了且应该被忽略”。它会阻止其它任何会被这个错误唤起的errorCaptured
钩子和全局的config.errorHandler
。
源码版本v2.6.14,位于./src/core/util/error.js。
handleError
、invokeWithErrorHandling
、globalHandleError
、logError
四个方法。
export function handleError(err: Error, vm: any, info: string) {
// 避免无限渲染
// See: https://github.com/vuejs/vuex/issues/1505
pushTarget();
try {
if (vm) {
let cur = vm;
// 向上查找$parent,直到不存在
// 注意了!一上来 cur 就赋值给 cur.$parent,
// 所以 errorCaptured 不会在当前组件的错误捕获中执行
while ((cur = cur.$parent)) {
// 获取钩子errorCaptured
const hooks = cur.$options.errorCaptured;
if (hooks) {
for (let i = 0; i < hooks.length; i++) {
try {
// 执行errorCaptured
const capture = hooks[i].call(cur, err, vm, info) === false;
// errorCaptured返回false,直接return,外层的globalHandleError不会执行
if (capture) return;
} catch (e) {
// 如果在执行errorCaptured的时候捕获到错误,会执行globalHandleError,
// 此时的info为:errorCaptured hook
globalHandleError(e, cur, "errorCaptured hook");
}
}
}
}
}
// 外层,全局捕获,只要上面不return掉,就会执行
globalHandleError(err, vm, info);
} finally {
popTarget();
}
}
export function invokeWithErrorHandling(
handler: Function,
context: any,
args: null | any[],
vm: any,
info: string
) {
let res;
try {
// 处理handle的参数并调用
res = args ? handler.apply(context, args) : handler.call(context);
// 判断返回是否为Promise 且 未被catch(!res._handled)
if (res && !res._isVue && isPromise(res) && !res._handled) {
res.catch((e) => handleError(e, vm, info + ` (Promise/async)`));
// issue #9511
// avoid catch triggering multiple times when nested calls
// _handled标志置为true,避免嵌套调用时多次触发catch
res._handled = true;
}
} catch (e) {
// 捕获错误后调用 handleError
handleError(e, vm, info);
}
return res;
}
function globalHandleError(err, vm, info) {
if (config.errorHandler) {
try {
// 调用全局的 errorHandler 并return
return config.errorHandler.call(null, err, vm, info);
} catch (e) {
// if the user intentionally throws the original error in the handler,
// do not log it twice
// 如果用户故意在处理程序中抛出原始错误,不要记录两次
if (e !== err) {
// 对在 globalHandleError 中的错误进行捕获,通过 logError 输出
logError(e, null, "config.errorHandler");
}
}
}
// 如果没有 errorHandler 全局捕获,则执行到这里,用 logError 错误
logError(err, vm, info);
}
function logError(err, vm, info) {
if (process.env.NODE_ENV !== "production") {
// 开发环境中使用 warn 对错误进行输出
warn(`Error in ${info}: "${err.toString()}"`, vm);
}
/* istanbul ignore else */
if ((inBrowser || inWeex) && typeof console !== "undefined") {
// 直接用 console.error 打印错误信息
console.error(err);
} else {
throw err;
}
}
总结:
handleError
:统一的错误捕获函数。实现子组件到顶层组件错误捕获后对errorCaptured hook
的冒泡调用,执行完全部的errorCaptured
钩子后最终执行全局错误捕获函数globalHandleError
;invokeWithErrorHandling
:包装函数,通过高阶函数的编程私思路,通过接收一个函数参数,并在内部使用try-catch
包裹后执行传入的函数;还提供更好的异步错误处理,当执行函数返回了一个Promise
对象,会在此对其实现进行错误捕获,最后也是通知到handleError
中(如果我们未自己对返回的Promise
进行catch
操作);globalHandleError
:调用全局配置的errorHandler
函数,如果在调用的过程中捕获到错误,则通过logError
打印所捕获的错误,以 ‘config.errorHandler’ 结尾;logError
:实现对未捕获的错误信息进行打印输出,开发环境会打印2种错误信息。
5.6 请求异常
axios
是一个基于promise
的网络请求库,支持拦截请求和响应,可以使用interceptors
处理响应错误。 一般接口 401
就代表用户未登录,需要跳转到登录页,让用户进行重新登录,但如果每个请求方法都需要写一遍跳转登录页的逻辑就很麻烦了,这时候就会考虑使用 axios
的拦截器来做统一处理,同理能统一处理的异常也可以在放在拦截器里处理。
// 主应用
service.interceptors.response.use(
response => {
return Promise.resolve(response.data);
},
error => {
let { response } = error;
if (response?.status === 401) {
store.commit('user/logoutClear');
routerInstance?.push({ name: 'login' });
}
return Promise.reject(error);
}
);
export const requestErrorInterceptor = error => {
console.log('error:', error);
if (!error) return;
let { response } = error;
// 401处理
if (response?.status === 401) return Message.warning('登录失效,请重新登录!');
// 普通报错处理
let msg = response?.data?.message || response?.data?.msg || error.message;
msg && Message.error(msg);
};
async submitForm(formName) {
try {
this.buttonLoading = true
const res = this.isAddMode
? await addRoute(reqBody)
: await editRoute(this.currentRoute.id, reqBody);
if (SUCCESS_CODE.includes(res?.code)) {
this.$message.success('操作成功');
return;
}
this.$message.error(res.message || '操作失败');
}
catch (error) {
requestErrorInterceptor(error);
} finally {
this.buttonLoading = false;
}
}
async submitForm(formName) {
this.buttonLoading = true;
const res = this.isAddMode
? await addRoute(reqBody)
.catch(error => requestErrorInterceptor(error))
.finally(() => (this.buttonLoading = false))
: await editRoute(this.currentRoute.id, reqBody)
.catch(error => requestErrorInterceptor(error))
.finally(() => (this.buttonLoading = false));
if (SUCCESS_CODE.includes(res?.code)) {
this.$message.success('操作成功');
return;
}
this.$message.error(res.message || '操作失败');
}
Promise.all([this.getServices(), this.getRoutes()])
.then(res => {
this.serviceList = res[0];
this.routeList = res[1];
this.getAllTableData();
})
.finally(() => (this.loading = false));
6. 总结
- 使用静态分析工具
eslint
、tslint
、stylint
、vetur
- 使用
typescript
- 可疑区域增加
try-catch
- 全局监控 JS 异常
window.onerror
- 全局监控静态资源异常
window.addEventListener
- 捕获没有
catch
的Promise
异常用unhandledrejection
Vue
异常使用Vue.config.errorHandler
、errorCaptured
Axios
请求统一异常处理用拦截器interceptors