Search code examples
javascriptes6-promise

Asynchronous action within javascript promise finally block


I am implementing a function that returns a Promise. In its implementation, I am calling another function, itself returning a Promise, on which I need to transform the result a bit.

Something like this:

function myDoStuff(params) {
    return actuallyDoStuff(params).then(
        (result) => { return "myTransformation " + result; }
    );
}

Now, I also need to call some cleanup code, whether this succeeds or fails. I could add a finally clause to the returned promise, but the problem is: what I need to do in the finally clause is also asynchronous (basically, another function returning a Promise again), and I need the returned promise to wait for the finalization to be done before it settles.

It seems I can't return a promise within the finally function (at least, it doesn't seem to be mentioned in https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally).

So, do I need to call the finalization from both then and catch, chaining the promises, or is there a way using a finally construct?

Calling finalization in both cases and preserving the error of the actuallyDoStuff results in ugly code:

function myDoStuff(params) {
    return actuallyDoStuff(params).then((result) => {
        return doFinalization().then(() => {
            return "myTransformation " + result;
        });   
    }, (err) => {
        return doFinalization().then(() => {
            throw err;
        }), () => {
            throw err;
        });
    });
}

Solution

  • You can return a promise from finally, and that will indeed hold up the chain. That promise just can't change the resolution value, and resolution value it returns will be ignored in favor of the original one. (It can change a resolution into a rejection, so you have to be careful there.)

    So just add onto the chain, if you want to allow cleanup to convert resolution to rejection if cleanup fails:

    function myDoStuff(params) {
        return actuallyDoStuff(params)
            .then(
                (result) => { return "myTransformation " + result; }
            )
            .finally(cleanup);
    }
    

    If you want to ignore errors from cleanup, you need to suppress them:

    function myDoStuff(params) {
        return actuallyDoStuff(params)
            .then(
                (result) => { return "myTransformation " + result; }
            )
            .finally(() => cleanup().catch(() => {}));
    }
    

    Examples:

    // Note this takes only 10ms
    function actuallyDoStuff(valueOrError, fail = false) {
      return new Promise((resolve, reject) => {
        setTimeout(fail ? reject : resolve, 10, valueOrError);
      });
    }
    
    // Note this takes a full second
    function cleanup(fail = false) {
      return new Promise((resolve, reject) => {
        setTimeout(fail ? reject : resolve, 1000, "cleanup done");
      });
    }
    
    function myDoStuff(...params) {
        return actuallyDoStuff(...params)
            .then(
                (result) => { return "myTransformation " + result; }
            )
            .finally(cleanup);
    }
    
    console.log("start with success");
    myDoStuff("success")
      .then(value => console.log("success", value))
      .catch(error => console.log("error", error))
      .finally(() => {
        console.log("Notice how there was a 1,010ms delay, and that the result was from actuallyDoStuff, not cleanup");
        console.log("start with error");
        myDoStuff("error", true)
          .then(value => console.log("success", value))
          .catch(error => console.error("error", error))
          .finally(() => {
            console.log("Notice how there was a 1,010ms delay");
           });
             });


    It's worth noting that now that async/await is here, you can also do this:

    async function myDoStuff(params) {
        try {
            const result = await actuallyDoStuff(params);
            return return "myTransformation " + result;
        } finally {
            await cleanup(); // Allows errors from cleanup
        }
    }
    

    or

    async function myDoStuff(params) {
        try {
            const result = await actuallyDoStuff(params);
            return "myTransformation " + result;
        } finally {
            await cleanup().catch(() => {}); // Suppresses errors from cleanup
        }
    }
    

    or with a try/catch in the finally (but it's more verbose):

    async function myDoStuff(params) {
        try {
            const result = await actuallyDoStuff(params);
            return "myTransformation " + result;
        } finally {
            try {
                await cleanup()
            } catch (e) { // As of ES2019, you could leave the `(e)` off
                          // That's already at Stage 4
            }
        }
    }