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;
});
});
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.
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.
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:
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:
- Safari (version 11+)
- Chrome (version 63+) (platform status)
- Supported in EDGE
- Firefox- in development
- Node.js (8.1.4+ under the --harmony-promise-finally flag) (issue)
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;
});
});
};
}());
<pre><code>...</code></pre>
tags in comments