为什么使用 Promise.finally 会导致未捕获的异常?

Why is this use of Promise.finally leading to uncaught exception?

我正在实现一个名为 timeout 的函数,以将承诺转换为如果未在最短时间内解决则将拒绝的承诺。

通过以下实现,当传递给 timeout 的承诺在分配的时间之前被拒绝时,我在 Chrome devtools 中收到一条 uncaught (in promise) 消息。

function timeout(promise, duration, message) {
    const timeoutPromise = new Promise((_, reject) => {
        const handle = setTimeout(() => {
            reject(new TimeoutError(message ?? "Operation timed out."))
        }, duration)
        promise.finally(() => clearTimeout(handle)) //problem line
    })
    return Promise.race([promise, timeoutPromise])
}

不过,如果我将其更改为以下内容,我似乎没有问题。

function timeout(promise, duration, message) {
    const timeoutPromise = new Promise((_, reject) => {
        const handle = setTimeout(() => {
            reject(new TimeoutError(message ?? "Operation timed out."))
        }, duration)
        const clear = () => clearTimeout(handle)
        promise.then(clear).catch(clear)
    })
    return Promise.race([promise, timeoutPromise])
}

我想了解在第二种情况下调用 then & catch 与在第一种情况下调用 finally 有何不同。

这是因为 finally 处理程序不同于 thencatch 处理程序。如果承诺被拒绝,finally 处理程序无法将拒绝更改为解决方案。就像 try/catch/finally 结构的 finally 块一样,它在很大程度上是透明的。

我在新书(第 8 章)中是这样描述的:

In the normal case, a finally handler has no effect on the fulfillment or rejection passing through it (like a finally block): any value it returns other than a thenable that is/gets rejected is ignored. But if it throws an error or returns a thenable that rejects, that error/rejection supersedes any fulfillment or rejection passing through it (just like throw in a finally block). So it can't change a fulfillment value — not even if it returns a different value — but it can change a rejection reason into a different rejection reason, it can change a fulfillment into a rejection, and it can delay a fulfillment (by returning a thenable that is ultimately fulfilled).

Or to put it another way: it's like a finally block that can't contain a return statement. (It might have a throw or might call a function or do some operation that throws, but it can't return a new value.

这是无法将拒绝转化为满足的示例:

Promise.reject(new Error("Failed"))
.finally(() => {
    console.log("finally handler returns null");
    return null;
});
Look in the browser's real console to see the unhandled rejection error.


在您的代码中处理它的另一种方法是用这个代替问题行:

promise.catch(() => {}).finally(() => clearTimeout(handle))

promise.catch(() => {}).then(() => clearTimeout(handle))

finally(或 then)处理程序之前将拒绝转换为完成(仅在此分支上)。