在上一篇文章中,我们详细回顾了JS错误捕获的原理,并了解了如何使用try...catch...和Promise.catch,所以现在让我们看一个稍微复杂的场景! 虽然是一个复杂的场景cocos-js 异步 个别加载失败,但其实很常见,就是一个函数中有多个后续的异步操作,而这些异步操作需要进行错误捕获。 代码说明如下:
async function stepOne() {
// 异步操作一
}
async function stepTwo() {
// 异步操作二
}
async function stepThree() {
// 异步操作三
}
// 使用 try...catch...对错误统一捕获
async function asyncRun() {
try{
const res1 = await stepOne()
const res2 = await stepTwo()
const res3 = await stepThree()
}catch(err) {
console.log(err)
}
}
// 使用 Promise.catch 对错误进行捕获
function runPromises() {
stepOne()
.then(res => {
stepTwo().then(res => {
stepThree().then(res => {})
})
})
.catch(err => {
console.log(err)
})
}
上面的代码示例中,try...catch...和Promise.catch用于统一捕获错误,在错误抛出后采用冒泡机制。 暂时不说这两种形式的优缺点。 想想我们的目的。 假设我们想要识别这些错误,并对不同异步操作的错误进行不同的处理。 这种情况下,简单的使用一个try...catch...氛围,否则Promise.then就无法实现。
最容易想到的解决方案是使用多个try...catch...来嵌套。 然而,有没有更优雅的方式呢?
看了网上的一些解决方案,总结了以下几种方法。 它们之间有一定的相似之处。 我从简单的开始一一介绍。
v1.0 等待 doSomething().catch(e=>e)
async function runAsync() {
const res1 = await stepOne().catch(e => e)
// 对res1的类型或值进行判断
if(...) {
// 如果出错
// 错误处理逻辑
}
const res2 = await stepTwo().catch(e => e)
// 对res2的类型或值进行判断
if(...) {
// 如果出错
// 错误处理逻辑
}
}
这个方法非常简单。 使用 Promise.catch 方法捕获错误并返回具有已解决状态和错误值的 Promise。
如果发生错误,则res1的值为error,所以这里一般使用条件语句来判断res1,然后再继续。
v1.1先报错
采用先返回错误的原则,这里的写法和Node.js、Golang是一样的。 也是之前写法的增强版,将值和错误数组一起返回。
async function runAsync() {
const [err1, res1] = await stepOne(val => [null, val]).catch(e => [e, null])
if(err1) {
// 如果出错
// 错误处理逻辑
}
const [err2, res2] = await stepTwo(val => [null, val]).catch(e => [e, null])
if(err2) {
// 如果出错
// 错误处理逻辑
}
}
这种写法比较实用。 如果我们的项目中只有一两个地方有这样的二次异步操作,需要进行错误处理就足够了。 当然技能特效,我们很快就会发现这种做法带来了大量的重复,并且不符合DRY原则。 例如:
在v2版本中,我们会进行适当的封装~
v1.2 处理catch中的错误并继续抛出
该方法适用于存在错误处理逻辑,并且当前函数的执行会被终止(返回)的场景。 与v1.1中的条件语句类似,处理错误并使用return。
async function runAsync() {
try{
const res1 = await stepOne().catch(err => {
// 处理错误1
// handleErr(err)
// 继续抛出错误
Promise.reject(err)
})
const res2 = await stepTwo().catch(err => {
// 处理错误2
// handleErr(err)
// 继续抛出错误
Promise.reject(err)
})
}catch(err) {
// 统一的错误处理逻辑
}
}
这里通过调用Promise.reject静态方法,可以继续抛出异步操作的错误,从而不执行后续逻辑,类似于if(err)return。 当然,这样使用typescript时,res1的返回值类型需要进行一些处理。
v2.0 抽象了try…catch…的逻辑
如果项目中存在很多上述场景,那么就需要进行适当的封装。 否则,我们只是笨拙地将嵌套的try...catch...转换成额外的条件判断+return语句。
首先想到的是将其提取为函数。
const handle = (fn: (...args: any[]) => Promise<{}>) => async (...args: any[]) => {
try {
return [null, await fn(...args)];
} catch(e) {
console.log(e, 'e.messagee');
return [e];
}
}
async function runAsync() {
const [err1, res1] = handle(stepOne)
if(err1) {
// 错误处理
// return
}
const res2 = handle(steoTwo)
}
上面的代码显示了句柄函数的简单版本。 您可以根据需要使用Error-first规则来封装handle函数。 调用异步操作时,调用句柄对其进行包装。
但实际上,这种封装是比较无用的,因为handle函数的catch子句中只有统一的错误处理逻辑。 如果需要针对性处理,就避免不了在主函数中进行二次判断和处理。
v2.1 自定义错误类型
这个思路参考了这篇文章
通过自定义错误类型+封装错误处理程序,解决上一个版本中handle函数无法识别错误类型的问题。 当然,这里的实现涉及到高阶函数以及继承Error来构造自定义的错误对象,可能会稍微复杂一些。
不过,这里的错误类型扩展方式还是值得学习的。
class DbError extends Error {
public errmsg: string;
public errno: number;
constructor(msg: string, code: number) {
super(msg);
this.errmsg = msg || 'db_error_msg';
this.errno = code || 20010;
}
}
class ValidatedError extends Error {
public errmsg: string;
public errno: number;
constructor(msg: string, code: number) {
super(msg);
this.errmsg = msg || 'validated_error_msg';
this.errno = code || 20010;
}
}
在此基础上,作者还扩展了使用装饰器来简化高阶函数逻辑的方法。 有兴趣的朋友可以进一步探索~
v2.2 将错误处理逻辑提取到装饰器/加载器中
我们刚才所做的就是将错误处理逻辑提取到一个函数中。 还有其他对代码干扰较小的方法,例如将其提取到加载程序中。 下面作者结合babel进行解析和插入。 主要思想是在遍历语法树进行处理时遇到的await表达式的节点前插入try...catch...代码块。链接如下
异步错误处理加载器
这种方式提高了代码编写风格的统一性,减少了侵入性,但同时也降低了灵活性。 而且,在实现此类加载器时cocos-js 异步 个别加载失败,需要针对不同的上下文做出更多的兼容性考虑。
所以我们还是需要根据具体业务场景具体分析。
最后总结一下。
从简单的处理方式到封装得更好的解决方案,其实还是那句话,一定要结合业务场景来看待。 没有最优解,有时甚至是简洁的写法try...catch...嵌套(少于三个)。