Promise.finally() is not natively supported in older browsers, thus we have to write a polyfill for it to make it work.
Same as the try…catch…finally block where no matter whether code runs in a try
block or catch
block, the finally
block will always be executed at the end, which can be used for a cleanup operations.
The same way for Promises we have .then()
for when promise resolves and .catch()
for when promise rejects and .finally()
block which will always run after any of those.
According to MDN –
The finally() method of a Promise schedule a function, the callback function, to be called when the promise is settled. Like then() and catch(), it immediately returns an equivalent Promise object, allowing you to chain calls to another promise method, an operation called composition.
Example
Input: function checkMail() { return new Promise((resolve, reject) => { if (Math.random() > 0.5) { resolve('Mail has arrived'); } else { reject(new Error('Failed to arrive')); } }); } checkMail() .then((mail) => { console.log(mail); }) .catch((err) => { console.error(err); }) .finally(() => { console.log('Experiment completed'); }); Output: Error: Failed to arrive "Experiment completed"
From the definition, we can conclude that to implement .finally()
,
- We have to take a callback function as an input and call this callback function when the promise is settled which is either after resolve or reject.
- Since there is no reliable way to tell if the promise was accepted or refused, a finally callback will not receive any argument.
- It will provide you with a promise that you can use to compose calls to other promise methods in a chain.
Promise.prototype.finally = function(callback) { if (typeof callback !== 'function') { return this.then(callback, callback); } // get the current promise or a new one const P = this.constructor || Promise; // return the promise and call the callback function // as soon as the promise is rejected or resolved with its value return this.then( value => P.resolve(callback()).then(() => value), err => P.resolve(callback()).then(() => { throw err; }) ); };
Test Case
Input: // This test case is from stack overflow. const logger = (label, start = Date.now()) => (...values) => { console.log(label, ...values, `after ${Date.now() - start}ms`); }; const delay = (value, ms) => new Promise(resolve => { setTimeout(resolve, ms, value); }); function test (impl) { const log = ordinal => state => logger(`${ordinal} ${impl} ${state}`); const first = log('first'); // test propagation of resolved value delay(2, 1000) .finally(first('settled')) .then(first('fulfilled'), first('rejected')); const second = log('second'); // test propagation of rejected value delay(Promise.reject(3), 2000) .finally(second('settled')) .then(second('fulfilled'), second('rejected')); const third = log('third'); // test adoption of resolved promise delay(4, 3000) .finally(third('settled')) .finally(() => delay(6, 500)) .then(third('fulfilled'), third('rejected')); const fourth = log('fourth'); // test adoption of rejected promise delay(5, 4000) .finally(fourth('settled')) .finally(() => delay(Promise.reject(7), 500)) .then(fourth('fulfilled'), fourth('rejected')); } test('polyfill'); Output: "first polyfill settled" "after 1005ms" "first polyfill fulfilled" 2 "after 1007ms" "second polyfill settled" "after 2006ms" "second polyfill rejected" 3 "after 2008ms" "third polyfill settled" "after 3006ms" "third polyfill fulfilled" 4 "after 3512ms" "fourth polyfill settled" "after 4000ms" "fourth polyfill rejected" 7 "after 4506ms"
Edge Cases
//This will be resolved with undefined Promise.resolve(2).then(() => {}, () => {}).then((val) => {console.log(val)}); // undefined //This will be resolved with 2 Promise.resolve(2).finally(() => {}).then((val) => {console.log(val)}); // 2 //Similarly, This will be fulfilled with undefined Promise.reject(3).then(() => {}, () => {}).then((val) => {console.log(val)}); // undefined //This will be fulfilled with 3 Promise.reject(3).finally(() => {}).then((val) => {console.log(val)}); // 3 //A throw (or returning a rejected promise) in the finally callback will reject //the new promise with the rejection reason specified when calling throw() Promise.reject(2).finally(() => { throw 'Parameter is not a number!' }).then((val) => {console.log(val)}); // 'Parameter is not a number!'