Serg Hospodarets Blog

Serg Hospodarets blog

Finally the Promise.prototype.finally() is available Serg Hospodarets Blog

Since the Promises were added in JavaScript, one of the biggest concerns was the absence of an ability to easily apply some code after the Promise is fulfilled (after both success and reject actions). There are many examples of such a need: showing a loader when the request to the server is in flight or even a simpler case- when we just want to log the completion of the operation regardless of how it finished.

Previously we had to include the function in both “on-success” and “on-reject” sections, which resulted in the code overhead and clearly showed the need of something like other libraries have, so meet the Promise.prototype.finally!

The problem

Again, the problem is clear- we’d like to execute the piece of code when the promise is fulfilled, and this should be aligned with the existing API.

Let’s consider the case when we read the data from the server. For example, let’s fetch the number of stars for the picked Github repo:

// HELPERS
function getStarsNumber(username, reponame) {
    return fetch(`https://api.github.com/repos/${username}/${reponame}`)
        .then(res => res.json())
        .then(data => data.stargazers_count)
        .catch(() => `Couldn't get the stars number`);
}

// EVENTS
formEl.addEventListener("submit", e => {
    e.preventDefault();

    starsNumberEl.innerHTML = "";

    getStarsNumber(usernameEl.value, reponameEl.value)
        .then((starsNumber) => {
            starsNumberEl.innerHTML = starsNumber;
        });
});

Demo

It works, but we don’t have a visual feedback for the user, so we’d like to show a spinner for a component during the request is in flight (quite a typical solution). Let’s modify our code (getStarsNumber() function) to start showing the loader right before the request is sent and hide after it’s finished (successfully or with an error):

// ANIMATION
function startLoadingAnimation() {
    componentEl.classList.add(componentLoadingClass);
}

function stopLoadingAnimation() {
    componentEl.classList.remove(componentLoadingClass);
}

// HELPERS
function getStarsNumber(username, reponame) {
    startLoadingAnimation();

    return fetch(`https://api.github.com/repos/${username}/${reponame}`)
        .then(res => res.json())
        .then(data => {
            stopLoadingAnimation();
            return data.stargazers_count;
        })
        .catch(() => {
            stopLoadingAnimation();
            return `Couldn't get the stars number`
        });
}

Immediately we have the code duplication and the need to always extract the code, which we want to execute on both success and reject, into a separate function (stopLoadingAnimation in our case).

Also, the code started being harder to read and we had to get rid of the arrow functions to return the proper values and execute the code to stop the animation.

Demo

Promise.prototype.finally() solves all this:

function getStarsNumber(username, reponame) {
    startLoadingAnimation();

    return fetch(`https://api.github.com/repos/${username}/${reponame}`)
        .then(res => res.json())
        .then(data => data.stargazers_count)
        .catch(() => `Couldn't get the stars number`)
        .finally(stopLoadingAnimation);
}

This code provides the separation of concerns and much more clear API.

Demo

Other libraries

The idea and the name finally came from the use cases and other libraries. E.g. jQuery provides deferred.always() which essentially does the same:

$.get( "test.php" ).always(function() {
  alert( "$.get completed with success or error callback arguments" );
});

Bluebird, Q and when libraries provide #finally methods you can use similarly:

return doSomething()
	.catch(handleError)
	.finally(cleanup);

The current state

The clear need for such an API resulted in a proposal: https://github.com/tc39/proposal-promise-finally.
Currently it’s on Stage 3 of the TC39 process.

Availability of Promise.prototype.finally() brings it to all the Promise-based API, like the global fetch() (used in the demos) and all the Promise-based code you have in your projects. Which means, as only .finally() is supported or polyfilled, you can use it across your codebase for Promises.

Promise.prototype.finally() is similar to synchronous finally{}

The name and the idea of finally method for Promises are quite similar to the synchronous finally{} block which we can use in try\catch constructions.
If you add there the asynchronous functions (async/await) you’ll see that this is the API which reflects the try/catch/finally behavior, which wasn’t clearly available before in Promises:

// HELPERS
async function getStarsNumber(username, reponame) {
    try {
        startLoadingAnimation();

        const res = await fetch(`https://api.github.com/repos/${username}/${reponame}`);
        const data = await res.json();
        return data.stargazers_count;
    } catch (err) {
        return `Couldn't get the stars number`;
    } finally {
        stopLoadingAnimation();
    }
}

// EVENTS
formEl.addEventListener("submit", async function (e) {
    e.preventDefault();

    starsNumberEl.innerHTML = "";

    starsNumberEl.innerHTML = await getStarsNumber(usernameEl.value, reponameEl.value);
});

Here is the async/await demo:

Demo

Similar to finally{} section in the synchronous code, Promise.prototype.finally() doesn’t get any arguments (like the success data or the reject error), which is aligned with the API and the purpose.

Support

Currently, the implementation is available in:

But the good news is that the polyfills are available and you can start using this today.

Polyfills

The spec spec-compliant polyfill is available here: https://github.com/es-shims/Promise.prototype.finally. You can integrate it in your projects and use.

If you use babel you can include the babel-polyfill (if you don’t use it so far), which will include this functionality
(it’s based on core-js, which in turn includes Promise.prototype.finally)

Here is the polyfill I used for the demos, so you can check how .finally() works under the hood:

(function () {
  // based on https://github.com/matthew-andrews/Promise.prototype.finally

  // Get a handle on the global object
  let globalObject;
  if (typeof global !== 'undefined') {
    globalObject = global;
  } else if (typeof window !== 'undefined' && window.document) {
    globalObject = window;
  }

  // check if the implementation is available
  if (typeof Promise.prototype['finally'] === 'function') {
    return;
  }

  // implementation
  globalObject.Promise.prototype['finally'] = function (callback) {
    const constructor = this.constructor;

    return this.then(function (value) {
      return constructor.resolve(callback()).then(function () {
        return value;
      });
    }, function (reason) {
      return constructor.resolve(callback()).then(function () {
        throw reason;
      });
    });
  };
}());

Links

Provide your code in <pre><code></code></pre> tags