先记住这个答案
finally 和 then 最大的区别有三点。第一,finally 回调没有参数,它不知道上一步是成功还是失败。第二,finally 的返回值会被忽略,上一步的成功值或失败原因原样传给下一步,而 then 返回什么,下一步就拿到什么。第三,只有在 finally 里抛错,或者返回一个 rejected 的 Promise,才会把结果改掉。所以 finally 只适合做关 loading、释放资源这种和结果无关的收尾工作。
finally回调没有参数,拿不到值也拿不到错误finally的返回值被忽略,原结果穿透下去finally里抛错会把结果改成这个新错误finally不会吞错误,失败照样要catch
用一段代码看清三个区别
下面把三种情况放在一起跑,每一步都 await,顺序就是打印顺序。
async function main() {
// 1. finally 回调没有参数
await Promise.resolve('data').finally((...args) => console.log('参数个数:', args.length))
// 2. 返回值被忽略,原来的值穿透下去
const v = await Promise.resolve('原始值').finally(() => '新值')
console.log('拿到:', v)
// 3. 失败原因也会穿透,finally 不会吞错误
await Promise.reject(new Error('请求失败'))
.finally(() => 'ignored')
.catch((err) => console.log('catch 到:', err.message))
// 4. finally 里抛错会覆盖原结果
await Promise.resolve('原始值')
.finally(() => { throw new Error('清理失败') })
.catch((err) => console.log('catch 到:', err.message))
}
main()查看输出与解释
参数个数: 0
拿到: 原始值
catch 到: 请求失败
catch 到: 清理失败第 2 步如果把 finally 换成 then(() => '新值'),v 就会变成 新值。这就是「加工者」和「旁观者」的区别:then 回调的返回值会成为新 Promise 的结果,finally 回调的返回值直接被丢弃。
第 3 步说明 finally 不是 catch。错误经过 finally 后依然是错误,后面没有 catch 的话,照样会报未处理的拒绝。
为什么设计成拿不到结果
finally 对应的是同步代码里 try...catch...finally 的 finally 块。同步的 finally 块本来就不接收返回值或异常,它的职责是「不管怎样都要做」,比如关掉文件句柄。Promise 的 finally 照搬了这个语义。
在它出现之前,要实现同样效果只能写 p.then(cleanup, cleanup)。但这样写有两个问题:cleanup 会收到值或错误作为参数;更麻烦的是,cleanup 的返回值会替换掉原来的结果,失败还会被转成成功。finally 解决的正是这两点,它等价于下面这种写法:成功时执行回调再把原值返回,失败时执行回调再把原错误重新抛出。
- 什么时候
finally会改变结果 - 只有两种情况。一是回调里同步
throw;二是回调返回一个最终 rejected 的 Promise。这两种情况下,原来的值或错误都会被新错误替换。返回一个 fulfilled 的 Promise 不会改结果,但链会等这个 Promise 完成后才继续,这可以用来在finally里做异步清理,比如等上报日志发完再往下走。
实际项目里最常见的用法
最典型的场景是关 loading。请求成功要关,失败也要关,以前要在 then 和 catch 里各写一遍,现在写一次就够。
- 用
finally关 loading - 比如提交表单时,先把
loading设成true,然后submitForm(data).then(showSuccess).catch(showError).finally(() => { loading = false })。用 async/await 写就是try { await submitForm(data) } catch (e) { showError(e) } finally { loading = false },两种写法效果一样。 这里有个顺序问题:finally放在catch后面,它执行时错误已经被处理了,链的最终结果是成功;放在catch前面,错误会穿过finally再被catch接住。关 loading 这种不关心结果的操作放哪都行,但如果finally后面还接了then,位置就会影响后面拿到的是什么。
容易答错的地方
- 以为
finally能拿到结果 - 很多人写
finally((res) => console.log(res)),结果打印出undefined。finally回调不接收任何参数,需要结果就在前面的then里处理。 - 以为
finally能处理错误 finally执行完,错误会继续往后传。只写finally不写catch,控制台照样会报Uncaught (in promise)。- 以为
finally里return能改结果 return普通值、return一个 fulfilled 的 Promise 都不会改结果。能改结果的只有抛错和返回 rejected Promise,而且改成的一定是失败,没法把失败改成成功。
面试官还会怎么问?
手写一个 Promise.prototype.finally 怎么写?
核心是 this.then(onSuccess, onFail)。成功时 return Promise.resolve(callback()).then(() => value),失败时 return Promise.resolve(callback()).then(() => { throw reason })。用 Promise.resolve 包一层,是为了在回调返回 Promise 时等它完成;后面的 then 把原值或原错误重新交出去。
finally 回调是同步执行还是异步执行?
和 then 一样是异步的,Promise 状态确定后作为微任务执行。而且因为内部多包了一层 then,finally 后面接的回调会比直接接 then 晚几个微任务,打印顺序题里偶尔会考到。
async 函数里 try...finally 的 finally 块,和 Promise 的 finally 一样吗?
语义基本一致,都是无论成功失败都执行。一个区别是同步风格的 finally 块里写 return 会覆盖 try 里的返回值,而 Promise 的 finally 回调里 return 普通值会被忽略。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。