简体   繁体   中英

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

I am implementing a function called timeout to convert a promise into a promise that will reject if not settled within a minimum amount of time.

With the following implementation, I get an uncaught (in promise) message in Chrome devtools when the promise passed to timeout rejects before the allotted time.

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])
}

If I change it to the following however, I seem to have no issues.

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])
}

I'm trying to understand how calling then & catch in the second scenario is different to calling finally in the first.

This is because finally handlers are different from then and catch handlers. If the promise is rejected, a finally handler can't change rejection to resolution. Like the finally block of a try / catch / finally structure, it's meant to be largely transparent.

Here's how I describe it in my new book (Chapter 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.

Here's an example of not being able to convert rejection to fulfillment:

 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.


Another way to deal with it in your code is to do this in place of the problem line:

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

or

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

That converts rejection to fulfillment (only on this branch) prior to the finally (or then ) handler.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM