本文详细解析JavaScript中Promise错误捕获机制,解释为何传统try-catch无法捕获异步错误,探讨Promise.reject与async/await的区别,并提供catch方法、unhandledrejection事件及兜底方案的最佳实践。
在异步任务执行过程中,准确捕获错误至关重要,这有助于开发者快速定位问题源头。
若对Promise机制及try-catch原理理解不足,常会遇到Promise内部错误无法被常规手段捕获的情况。本文将深入探讨这些场景及其成因。
try-catch机制解析
try catch仅能捕获当前执行上下文中的同步错误,即它无法处理异步任务引发的异常。具体场景如下所示:
try {
throw "程序执行遇到了一些错误";
} catch(e) {
console.log(e)
}
// 控制台会输出:程序执行遇到了一些错误
上述代码成功捕获了同步错误,开发者可在程序中进行相应的错误处理逻辑。
然而,面对异步任务时,try-catch显得力不从心,无法正确捕获错误:
try {
setTimeout(() => {
throw "程序执行遇到了一些错误"
})
} catch(e) {
console.log(e);
}
// 控制台输出:Uncaught 程序执行遇到了一些错误;
或者采用以下代码结构:
try {
Promise.reject('程序执行遇到了一些错误');
} catch(e) {
console.log(e);
}
// 控制台输出:Uncaught (in promise) 程序执行遇到了一些错误
上述代码均无法捕获错误,根本原因在于:try-catch仅针对同步错误有效。
何为同步错误?
当错误发生在同一事件循环内的同一个任务队列中时,对该任务上下文而言,即视为同步错误。
setTimeout与Promise被称为不同的任务源,注册自不同任务源的回调函数会被分配至不同的任务队列。
setTimeout注册的任务会被放入宏任务队列。
Promise注册的任务会被放入微任务队列。
- 补充说明:setTimeout是由宿主浏览器发起的任务,通常被归类为宏任务。
- 而Promise是由JavaScript引擎发起的任务,通常被归类为微任务。
在第一次事件循环中,JavaScript引擎将整个script代码块视为一个宏任务执行。执行完毕后,引擎会检查本次循环中是否存在微任务,若存在则依次从微任务队列中读取并执行所有微任务,随后再读取宏任务队列中的任务执行,如此循环往复。
JavaScript的执行顺序表现为每次事件循环中宏任务与微任务的不断交替执行。
再次审视setTimeout中抛出的错误,该错误已不在try-catch所在的事件循环范围内,因此属于异步错误,无法被try-catch捕获。
同理,Promise.reject()虽为同步执行,但其reject的内容处于另一个微任务循环中,对try-catch而言同样属于异步范畴,故这两类错误均无法被捕获。
Promise.reject机制解析
理解Promise.reject需先了解其返回值,Promise.reject返回的是一个Promise对象,请注意:是Promise对象。
Promise对象在任何时刻均为合法对象,它既非错误也非异常。因此,在任何实现中,直接对Promise.reject或返回Promise对象的调用使用try-catch均无意义,因为正常对象永远不会触发catch捕获逻辑。
假设存在如下代码:
function getData() {
Promise.reject('遇到了一些错误');
};
function click() {
try {
getData();
} catch(e) {
console.log(e);
}
}
click(); // 我们模拟业务场景中的click事件
// 控制台输出: Uncaught (in promise) 遇到了一些错误
既然Promise已通过reject抛出错误,为何try-catch无法捕获?
首先需明确,判断函数错误是否可被捕获,可将函数调用的返回值替换至调用处,观察其是否为错误对象。
上述getDate()调用最终会被替换为undefined;
对于未明确return的函数调用,其返回值始终为undefined,因此代码如下:
function click() {
try {
undefined;
} catch(e) {
console.log(e);
}
}
具备JavaScript基础知识的开发者可知,这不属于异常,代码会正常执行,不会进入catch分支。
或许有人提出另一种思路:将Promise.reject直接返回,代码变为:
function getData() {
return Promise.reject('遇到了一些错误');
};
function click() {
try {
getData();
} catch(e) {
console.log(e);
}
}
click();
Promise.reject返回的是Promise对象,即对象而非错误。因此在try-catch中完成getData()调用后,得到的是一个Promise对象,这属于正常对象,不会被catch捕获,故该try-catch依然无效。
于是又出现一种思路:在调用处使用Promise的catch方法进行捕获,代码变为:
function getData() {
return Promise.reject('遇到了一些错误');
};
function click() {
try {
getData().catch(console.log);
} catch(e) {
console.log(e);
}
}
click();
此方案可行,reject的错误可被捕获,但这归功于Promise内部消化机制,而非try-catch,因此此处的try-catch依然无实际意义。
Promise异常捕获解决方案
Promise异常是最常见的异步异常,其内部错误基本被包装为Promise对象后传递。解决Promise异步捕获的整体思路主要有两种:
- 使用Promise的catch方法在内部进行消化处理;
- 使用async和await将异步错误转换为同步错误,再由try-catch捕获。
Promise.catch方法应用
对于Promise.reject中抛出的错误,或Promise构造器中抛出的错误,亦或then中出现的错误,无论是运行时错误还是通过throw主动抛出的,原则上均可被catch捕获。
示例如下:
function getData() {
Promise.reject('这里发生了错误').catch(console.log);
}
function click() {
getData();
}
click();
或者在调用处进行捕获,但这要求被调用的函数必须返回Promise对象;
function getData() {
return Promise.reject('程序发生了一些错误');
}
function click() {
getData().catch(console.log);
}
click();
上述两种方案均有效。事实上,建议在业务逻辑允许的情况下,将Promise始终通过return向上传递,以便错误向上冒泡,同时配合**unhandledrejection**事件进行兜底处理。
async/await异步转同步处理
使用async和await可将异步函数调用在语义上转化为同步执行效果,从而允许使用try-catch进行统一错误处理。
例如:
function getData() {
return Promise.reject('程序发生错误');
}
async function click() {
try {
await getData();
} catch(e) {
console.log(e);
}
}
click();
需注意,若getData方法未编写return语句,则无法将Promise对象向上传递,调用处的await接收到的将是展开后的undefined,依旧无法进行错误处理。
注意事项
若函数内部处理了Promise异步对象,原则上其处理结果也应为Promise对象。对于需进行错误捕获的场景,Promise对象应始终通过return向上传递。
兜底方案
一般情况下,若同步错误未被捕获,该错误所在的事件循环将终止。因此,在开发阶段未捕获的错误,使用一种方法进行兜底处理十分必要。
对于同步错误,可定义window.onerror进行兜底处理,或使用window.addEventListener('error', errHandler)来定义兜底函数。
对于Promise异常,则可同步使用window.onunhandledrejection或者window.addEventListener('unhandledrejection', errHandler)来定义兜底函数。
then方法第二个参数与Promise.catch方法的区别
Promise中的then的第二个参数和catch有什么区别?
- reject用于抛出错误,属于Promise类的静态方法。
- catch用于处理异常,属于Promise实例的方法。
区别
主要区别在于,若在then的第一个函数中抛出异常,后续的catch能捕获到,但then的第二个参数却无法捕获。
then的第二个参数和catch在捕获信息时遵循就近原则。若为Promise内部报错,reject抛出错误后,当then的第二个参数和catch方法同时存在时,仅then的第二个参数能捕获到;若then的第二个参数不存在,则由catch方法捕获。
问题探讨:then方法的连续调用,如何知道是第几个then方法报错了呢。
new Promise((resolve,reject) => {
setTimeout(() => {
resolve(1);
}, 1000)
}).then(res => {
console.log(res);
return new Promise((resolve,reject) => {
reject('第一个then方法报错了');
})
}).then(res => {
console.log(res);
return new Promise((resolve,reject) => {
reject('第二个then方法报错了');
})
}).catch(err => {
console.log(err);
})总结
到此这篇关于JavsScript中Promise错误捕获的文章就介绍到这了,更多相关JS Promise错误捕获内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!