Search code examples
javascriptpromise

Why must I add a "then" after "finally" in order to chain promises?


Per this fiddle, if I try to call a second promise function from the .finally() block of the first promise, the second promise resolves but the resolve value is "undefined". However, if I call the second promise function in a .then block after the .finally block, everything works as expected. Why can't I call the second promise in the .finally block of the first promise?

Example that fails (Promise 2 value is "undefined"): https://jsfiddle.net/scemohkb/

function res(v) { return new Promise((resolve, reject) => { resolve(v); }) }
function rej(v) { return new Promise((resolve, reject) => { reject(v); }) }

rej(1)
.then((val) => {
    console.log(`Value1: ${val}`);
})
.catch((e) => {
    console.error(`Caught 1: ${e}`);
})
.finally(() => {
    console.log("Finally 1");
    return res(2);
})
.then((v) => {
    console.log(`Value2: ${v}`); // THIS DOESN'T WORK: v is undefined
})
.catch((e) => {
    console.log(`Caught 2: ${e}`);
})
.finally(() => {
    console.log("Finally 2");
});

Example that works: (Promise 2 returns the correct value): https://jsfiddle.net/scemohkb/1/

function res(v) { return new Promise((resolve, reject) => { resolve(v); }) }
function rej(v) { return new Promise((resolve, reject) => { reject(v); }) }

rej(1)
.then((val) => {
    console.log(`Value1: ${val}`);
})
.catch((e) => {
    console.error(`Caught 1: ${e}`);
})
.finally(() => {
    console.log("Finally 1");
})
.then(() => {
    return res(2);
})
.then((v) => {
    console.log(`Value2: ${v}`); // This works: v is the correct value.
})
.catch((e) => {
    console.log(`Caught 2: ${e}`);
})
.finally(() => {
    console.log("Finally 2");
});

In example 1, the second promise function still returns correctly upon calling resolve() and the .then block is executed, but the value sent to resolve() is ignored and instead the .then() function receives "undefined".

What is different about the .finally() function that I'm missing, that prevents this from happening? It would seem to make sense for the cleanest chaining of multiple promises that can resolve or reject independently, vs. adding another .then() function after the .finally().


Solution

  • Too long to continue in the comments, so I'm posting this as an answer:

    Let's say I have 10 sequential Promise-based functions to call. I want each one to be run, then regardless of the result, the next one should be run after either success or fail. So far, I'm seeing that adding a .then() after each .finally() lets me chain them all together. Is there anything wrong with this paradigm?

    Yes. You don't need .finally.

    Promise.prototype.finally is not meant to be a "keep this chain going regardless of whether or not the previous Promise resolved or rejected". It can't, it doesn't have access to the state of the preceding Promise. The callback you pass it does not take parameters, and its return value is ignored. That actually makes sense if you think about it in terms of try/catch/finally: the code in the finally block does not have access to the variables scoped to the try and catch blocks.

    What .finally is actually meant for is use cases like cleaning up resources that need to be freed regardless of outcome like closing file handles and web socket subscriptions. It's only useful at the tail end of the chain for cleanup, which is why it's called "finally".

    That still leaves the question of how to do what you're after here, which is simple if tedious: have .catch handlers that return a meaningful value so that the computation can proceed. Something like this:

    const a = async (n = 0) => {
        if (Math.round(Math.random())) {
            throw new Error('oops');
        }
    
        return 5 + n;
    };
    
    const handleError = (err) => {
        console.error(err);
        return 0;
    } 
    
    Promise.resolve(7)
      .then(a)
      .catch(handleError)
      .then(a)
      .catch(handleError)
      .then(a)
      .catch(handleError)
      .then(console.log);
    

    Now IRL you probably won't be calling the same function and using the same error handler over and over again: you might be able to abstract out some bits but you'll likely need to write bespoke callbacks at each step of the chain. Still this can hopefully serve as an illustration of how this works.

    Playground