JavaScript Promise的.safe():轻松忽略错误,让代码更简洁易读
在做了十几年的工程师之后,我知道这篇文章开头应该怎么写;因此,为了避免被有影响力的观点限制,我必须这样说以避免被权威意见牵制。
我知道在你没有控制权的 JavaScript 对象原型上定义属性并不是推荐的做法。
这样一来,我终于决定定义一个 Promise.prototype.safe()
方法,和它一起的还有原生的 Promise.prototype.then()
和 Promise.prototype.catch()
方法。
图片由 Lincoln W Daniel 摄制
你为什么要这么做呢?那么,让我来给你演示一下我经常经历的一个常见流程,我这个流程支持每年数百万访客和客户,我旗下的多个在线平台,例如ManyStories.com和OurTransfers.com。
这个过程的目标是通过一个较为复杂的过程从数据库中获取资源或创建资源,而这个过程无法仅通过数据库引擎的插入或更新功能来实现。
这个过程:
- 从数据库里取资源
- 如果找到了资源,就把它返回给调用者。
- 否则,就去做相应的处理。
- 更新或添加资源。
- 把资源返回给调用者。
async 确保故事用户关联({ storyId, userId }) {
try {
const dbRelation = await StoryUserRelationData.get({
storyId,
userId,
})
if (dbRelation) {
return dbRelation
}
// 执行一些工作
const extraData = await doSomeBusinessLogic()
return StoryUserRelationData.upsert({
extraData,
storyId,
userId,
})
} catch(err) {
// 处理异常
}
}
正如你看到的,调用 StoryUserRelation.get()
可能会导致错误,例如当资源不存在时。在这种情况下,我通常不会理会这个错误,因此,即使无法从数据库加载资源,我也希望进行创建或更新记录。
当然,错误可能是由于网络偶尔不稳定,这并不能告诉我资源是否实际上存在于数据库中,不过这没关系。因为如果资源确实存在,StoryUserRelationData.upsert()
操作就不会重复创建记录。此外,由于 get()
方法 99% 的时间都能正常工作,告诉我资源是否存在,所以对于那 1% 的时间里需要额外工作的情况,我也能接受。这些多余的步骤在 upsert()
方法没有创建新资源时就不起作用了。
所以我不喜欢使用 try/catch 块,因为它们冗长且繁琐,并且嵌套了大量的代码块,我尽量避免这种情况。我也并不总是这样处理 Promise.catch((err) => { /* 安全地忽略错误 */ })
。
为了这个目的,我想简化承诺的逻辑。这些年来,我使用了以下方式来实现这个目标。
/*
* 运行提供的 Promise 并安全处理任何捕获的错误。
*/
export function promiseDone<T>(promise: Promise<T> | (() => Promise<T>)) {
if (Array.isArray(promise)) {
promise = Promise.allSettled(promise)
}
if (lodash.isFunc(promise)) {
try {
promise = promise()
} catch (err) {
// 安全处理:忽略错误
}
}
promise = Promise.resolve(promise)
return promise.catch(() => {
// 安全处理:忽略错误
return
})
}
有了这个函数定义之后,我会这样修改我的流程,如下所示:
import { promiseDone } from '../utils/promises'
async 保证故事用户关联({ storyId, userId }) {
const dbRelation = await promiseDone(StoryUserRelationData.get({
storyId,
userId,
}))
if (dbRelation) {
return dbRelation
}
// 执行一些操作
const extraData = await 业务操作()
return StoryUserRelationData.upsert({
extraData,
storyId,
userId,
})
}
请观察这段代码 const dbRelation = await promiseDone(get())
。
这要好得多,但我不喜欢必须导入并将我的承诺调用包裹在一个函数中。毕竟,这种方法我已经用了多年,运作得很好。因此,我决定进一步简化我的过程,以减少代码输入量,并让代码更易读。
为了达到这一点,我做了一些有经验的工程师通常会警告你不要做的事情。不过,我认为不应该死守教条和硬性规定。只要你清楚你在做什么,知道风险在哪儿,并且有办法减轻这些风险,大多数事情都可以试试。
有了免责声明之后,希望我的评论区现在可以远离极端言论,我们可以继续:我改进后的解决方案了,
Promise.prototype.safe = function() {
return this.catch((error) => {
// 安全处理:忽略错误
})
}
// 我没有这么写
// Promise.prototype.safe = Promise.prototype.safe || myFunction
// 因为我不知道ECMAScript (JS)将来会如何实现这一点。
// 因为他们可能会有不同的实现方式,这可能会导致我的代码出问题。
我们现在在Promise
类的原型上定义了一个名为safe()
的方法。在这个方法里,我们会捕获任何可能出现的错误并选择忽略它们。因为在catch()
回调中我们不需要返回任何内容,因此不重新抛出错误就能达到忽略错误的目的。
另外,因为我们返回的是this
,即Promise
实例,所以我们还可以继续正常使用.then()
和.catch()
。
如果你在使用 TypeScript,你可以在自己的 promise.d.ts
定义文件中定义这个属性。
/**
* 表示异步操作完成的接口
*/
interface Promise<T> {
/**
* 仅在 Promise 解析时附加一个回调函数。
* 捕获并忽略由于 Promise 被拒绝而产生的任何错误。
* @param onfulfilled 当 Promise 解析时要执行的回调。
* @returns 一个表示回调执行完成的 Promise。
*/
safe<TResult1 = T, TResult2 = never>(
onfulfilled?: ((value?: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
): Promise<TResult1 | TResult2>
}
这使您能够继续享受您的 IDE/代码编辑器的 IntelliSense 等自动完成功能以及 TypeScript 的类型安全特性带来的好处。
就这样,我的承诺就变成了:
async 确认故事与用户的关联({ storyId, userId }) {
const dbRelation = await StoryUserRelationData.get({
storyId,
userId,
}).safe()
if (dbRelation) {
return dbRelation
}
// 进行一些操作
const extraData = await doSomeBusinessLogic()
return StoryUserRelationData.upsert({
extraData,
storyId,
userId,
})
}
请注意 const dbRelation = await get().safe()
这段代码。在我看来,这比我们最初的实现要简洁得多。
如果 StoryUserRelationData.get()
抛出错误,dbRelation
将是 undefined
。
正如我之前提到的,我们也可以像我们平时那样链式调用更多Promise方法。
const promisedResult = await new Promise((resolve, reject) => { ... })
// 具体实现省略
// .safe() // safe 方法未定义,可能需要额外处理或注释说明
.then() // 处理 resolve 的回调
.catch() // 处理 reject 的回调
.finally() // 无论 resolve 还是 reject,都会执行的回调
这对我来说效果非常好。对其他人来说,它可能简直就是魔鬼的杰作。幸运的是,我不相信有魔鬼这种东西,也不信他的对立面。我只相信以一种合理且易于维护和扩展的方式完成工作。
共同学习,写下你的评论
评论加载中...
作者其他优质文章