Promise.finally() polyfill

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!'