承诺落空?
漏掉错误,丢失结果……
JavaScript Promise 真的坏了吗?
过去式
关于 Promise 最常见的迷思之一,就是它所谓的错误处理缺陷。
很多年前,Promise 处理错误确实很糟糕。但后来投入了大量工作来修复它。
于是,它被修复了,甚至被广泛部署。
众人欢呼
遗憾的是,有些人并没注意到。
现代式
这个迷思依然存在,我随处可见:Medium 上的热门文章、DZone 上以及许多其他来源。
我承认,甚至连“官方”资源和文档提供的也大多是拙劣的示例和坏习惯。这些例子经常被用来“证明” Promise 不好用。有些甚至建议使用会让情况变得更糟的“疗法”。(注:链接已移除)
远离麻烦的准则
- Promise 需要有所依托
- 务必从函数中
return。
- 务必从函数中
- 使用真正的
Error实例- 务必使用
Error实例。
- 务必使用
- 在有意义的地方处理错误
- 务必至少使用一次
.catch()。
- 务必至少使用一次
- 通过具名函数增加清晰度 🦄✨
- 优先使用具名函数。
#1 Promise 需要有所依托
务必从函数中 return,这一点至关重要。
Promise 回调函数在 .then(callback) 和 .catch(callback) 中遵循特定的模式。
每个返回值都会被传递给下一个 .then() 的回调函数。
function addTen(number) { return number + 10;}
Promise.resolve(10) // 10 .then(addTen) // 20 .then(addTen) // 30 .then(addTen) // 40 .then(console.log) // logs "40"“始终返回”的额外好处:代码更容易进行单元测试。
问题: 这里创建了多少个不同的 Promise 状态(已解决和已拒绝)?
问题: 在前面的示例中创建了多少个 Promise?
#2 使用真正的 Error 实例
JavaScript 在错误处理方面有一种有趣的特性(这同时适用于异步和同步代码)。
[查看 repl.it 示例:throwing errors in javascript]
为了获取有关行号和调用栈的有用细节,你必须使用 Error 实例。像在 Python 或 Ruby 中那样抛出字符串在 JS 里是行不通的。
虽然 JavaScript 看起来能处理 throw "string",你也能在 catch 处理器中看到这个字符串。但是,你看到的仅仅是数据本身。它不会包含任何之前的栈帧(stack frames)。
正确的 new Error 示例:
throw new Error('message') // ✅Promise.reject(new Error('message')) // ✅throw Error('message') // ✅Promise.reject(Error('message')) // ✅以下是常见的反模式:
throw 'error message' // ❌Promise.reject(-42) // ❌#3 在有意义的地方处理错误
Promise 提供了一种优雅的错误处理方式,即使用 .catch()。它本质上是一种特殊的 .then() —— 专门用来处理前面任何 .then() 中抛出的错误。让我们看个例子……
Promise.resolve(42) .then(() => 'hello') .catch(() => console.log('will not get hit')) .then(() => throw new Error('totes fail')) .catch(() => console.log('WILL get hit'))虽然 .catch() 看起来像 DOM 事件处理器(比如 click、keypress),但它的位置至关重要,因为它只能“捕获”在它上方抛出的错误。
覆盖错误其实非常简单:在 .catch() 回调中返回一个非错误值,Promise 链就会切换回按顺序执行 .then() 回调。(实际上就是这样。)
尝试理解下面这个例子的执行顺序:
Promise.resolve(42) .then(() => 'hello') .then(() => throw new Error('totes fail')) .catch(() => { return 99 }) .then(num => num + 1) .then(console.log) // 预期输出: 100理解这个执行序列才是关键。
虽然这个例子很傻,但它的目的是为了说明 Promise 中错误和数据的流动方式。
以下是执行序列的简述:
- 初始值为 42。
- 下一个方法始终返回
hello。 - 我们忽略之前的值,并抛出一个带有
'totes fail'消息的错误。 .catch()拦截了错误,转而返回99,该值将由随后的任何.then()处理。- 递增
num,返回100。 console.log方法接收到100并将其打印出来!:tada:
思考题: 当两个 .catch() 连续出现时会发生什么?第二个会执行吗?你能想到这种用法的场景吗?
思考题: .catch() 如何忽略错误?你该如何防止错误导致 Promise.all 提前退出?
#4 使用具名函数增加清晰度 🦄✨
对比以下两个例子的可读性:
匿名函数: ❌
Promise.resolve(10) // 10 .then(x => x * 2) // 20 .then(x => x / 4) // 5 .then(x => x * x) // 25 .then(x => x.toFixed(2)) // "25.00" .then(x => console.log(x)) // 预期输出: "25.00"具名函数: ✅
Promise.resolve(10) // 10 .then(double) // 20 .then(quarter) // 5 .then(square) // 25 .then(format) // "25.00" .then(log) // 预期输出: "25.00"
const double = x => x * 2const quarter = x => x / 4const square = x => x * xconst format = x => x.toFixed(2)const log = x => console.log(x)额外红利: ✅
兼容数组方法!!!
你可以将这些具名函数复用到 Array.prototype 的方法中,包括 .map()、.filter()、.every()、.some()、.find()!
集合流水线(Collection pipelines)简直无敌:
// 简直一模一样 :mindblown:
[10, 20] // [ 10, 20 ] .map(double) // [ 20, 40 ] .map(quarter) // [ 5, 10 ] .map(square) // [ 25, 100 ] .map(format) // [ "25.00", "100.00" ] .map(log) // 预期输出两行: "25.00", "100.00"如果你不想写这种线性风格的代码……既然你已经有了这些简单的函数!
你可以按需使用它们:
// 嵌套模式// ❌ 但请千万别这么写
const result = format(square(quarter(double(10))))
log(result)// 预期输出: "25.00"为什么嵌套函数是一种反模式?
- 对大多数人来说可读性极差。
- git diff 无法直观地显示是谁改动了哪一部分。
- 很难在嵌套函数的中间环节进行调试或记录日志。