Promise.prototype.finally() recently reached stage 4 of the TC39 proposal process. This means the Promise.prototype.finally() proposal was accepted and is now part of the latest draft of the ECMAScript spec, and it is only a matter of time before it lands in Node.js. This article will show you how to use Promise.prototype.finally() and how to write your own simplified polyfill.

What is Promise.prototype.finally()?

Suppose you've created a new promise:

const promiseThatFulfills = new Promise((resolve) => {
  // Calling `resolve()` is how you fulfill a promise. "Fulfilled" and "resolved" are different
  // concepts: if you call `resolve()` with a value that is not a promise, the promise will
  // become fulfilled. However, if you call `resolve()` with a promise, the outer promise will
  // still be pending until the inner promise fulfills or rejects.
  setTimeout(() => resolve('Hello, World'), 1000);
});

const promiseThatRejects = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error('whoops!')), 1000);
});

You can chain promises together using the .then() function.

promiseThatFulfills.then(() => console.log('Will print after about 1 second'));
promiseThatRejects.then(null, () => console.log('Will print after about 1 second'));

Note that .then() takes two function parameters. The first is onFulfilled(), which gets called if the promise is fulfilled. The second is onRejected(), which gets called if the promise rejects. A promise is a state machine that can be in one of 3 states:

  • Pending: the underlying operation is in progress and the promise has neither fulfilled nor rejected yet
  • Fulfilled: the underlying operation has completed successfully and the promise now has an associated value
  • Rejected: the underlying operation failed for some reason and the promise now has an associated error

Furthermore, a promise that is fulfilled or rejected is called "settled."

While .then() is the core mechanism for promise chaining, it isn't the only one. Promises also have a .catch() function that is handy for error handling.

promiseThatRejects.catch(() => console.log('Will print after about 1 second'));

The .catch() function is just a convenient shorthand for .then() with an onRejected handler and no onFulfilled handler:

promiseThatRejects.catch(() => console.log('Will print after about 1 second'));
// Equivalent
promiseThatRejects.then(null, () => console.log('Will print after about 1 second'));

Like .catch(), the .finally() function is a convenient shorthand for .then(). The difference is that .finally() executes a function onFinally when the promise is settled, that is, when it is either fulfilled or rejected. The .finally() function is not part of any Node.js release at the time of this writing, but the promise.prototype.finally module on npm has a polyfill.

const promiseFinally = require('promise.prototype.finally');

// Add `finally()` to `Promise.prototype`
promiseFinally.shim();

const promiseThatFulfills = new Promise((resolve) => {
  setTimeout(() => resolve('Hello, World'), 1000);
});

const promiseThatRejects = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error('whoops!')), 1000);
});

promiseThatFulfills.finally(() => console.log('fulfilled'));
promiseThatRejects.finally(() => console.log('rejected'));

The above script will print both 'fulfilled' and 'rejected', because the onFinally handler gets called when the promise is settled, regardless of whether it is fulfilled or rejected. However, the onFinally handler does not receive any parameters, so you can't tell whether the promise was fulfilled or rejected.

The finally() function returns a promise, so you can chain more .then(), .catch(), and .finally() calls onto the return value. The promise that finally() returns will fulfill to whatever the promise it was chained onto would fulfill to. For example, the below script will still print 'foo', even though the onFinally handler returns 'bar'.

const promiseFinally = require('promise.prototype.finally');

// Add `finally()` to `Promise.prototype`
promiseFinally.shim();

Promise.resolve('foo').
  finally(() => 'bar').
  // Will print 'foo', **not** 'bar', because `finally()` acts as a passthrough
  // for fulfilled values and rejected errors
  then(res => console.log(res));

Similarly, the below script will still print 'foo', even though the onFinally function didn't trigger any errors.

const promiseFinally = require('promise.prototype.finally');

// Add `finally()` to `Promise.prototype`
promiseFinally.shim();

Promise.reject(new Error('foo')).
  finally(() => 'bar').
  // Will print 'foo', **not** 'bar', because `finally()` acts as a passthrough
  // for fulfilled values and rejected errors
  catch(err => console.log(err.message));

The above script demonstrates an important detail of working with finally(): finally() does not handle promise rejections for you. How finally() handles promise rejections merits more careful study.

Error Handling

The finally() function is not meant to handle promise rejections. As a matter of fact, it explicitly rethrows errors after your onFinally() function executes. The below script will print an unhandled promise rejection warning.

const promiseFinally = require('promise.prototype.finally');

// Add `finally()` to `Promise.prototype`
promiseFinally.shim();

const promiseThatRejects = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error('whoops!')), 1000);
});

promiseThatRejects.finally(() => console.log('rejected'));
$ node finally.js
rejected
(node:5342) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 2): Error: whoops!
(node:5342) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
$

Like with try/catch/finally, you typically want to chain .finally() after a .catch().

const promiseFinally = require('promise.prototype.finally');

// Add `finally()` to `Promise.prototype`
promiseFinally.shim();

const promiseThatRejects = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error('whoops!')), 1000);
});

promiseThatRejects.
  catch(() => { /* ignore the error */ }).
  finally(() => console.log('done'));

However, the finally() function returns a promise, so there's nothing stopping you from chaining .catch() after .finally(). In particular, if your onFinally handler can error out, for example if it makes an HTTP request, you should add a .catch() at the end to handle any errors that may occur.

const promiseFinally = require('promise.prototype.finally');

// Add `finally()` to `Promise.prototype`
promiseFinally.shim();

const promiseThatRejects = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error('whoops!')), 1000);
});

promiseThatRejects.
  finally(() => console.log('rejected')).
  // No unhandled promise rejection because there's a `.catch()`
  catch(() => { /* ignore the error */ });

Simplified Polyfill

I think that the easiest way to really grok something is to write your own implementation. The .finally() function is a good candidate for this because the official polyfill is only 45 lines, and most of it isn't necessary for a simple proof of concept.

Here are the test cases this simple finally() polyfill will work to address. The below script should print 'foo' 5 times.

// Return value is ignored, promise fulfills normally
Promise.resolve('foo').
  finally(() => 'bar').
  then(res => console.log(res));

// Return value is ignored, promise rejects normally
Promise.reject(new Error('foo')).
  finally(() => 'bar').
  catch(err => console.log(err.message));

// Error in the `onFinally` handler, should reject with the new error
Promise.reject(new Error('bar')).
  finally(() => { throw new Error('foo'); }).
  catch(err => console.log(err.message));

// The `onFinally` handler returns a rejected promise,
// should reject with the new error
Promise.reject(new Error('bar')).
  finally(() => Promise.reject(new Error('foo'))).
  catch(err => console.log(err.message));

// The `onFinally` handler returns a promise, should wait until the promise
// settles before continuing.
const start = Date.now();
Promise.resolve('foo').
  finally(() => new Promise(resolve => setTimeout(() => resolve(), 1000))).
  then(res => console.log(res, Date.now() - start));

Below is the simplified polyfill.

// Add `finally()` to `Promise.prototype`
Promise.prototype.finally = function(onFinally) {
  return this.then(
    /* onFulfilled */
    res => Promise.resolve(onFinally()).then(() => res),
    /* onRejected */
    err => Promise.resolve(onFinally()).then(() => { throw err; })
  );
};

The key idea behind this implementation is that the onFinally handler may return a promise. If it does, you need to .then() on that promise and resolve or reject with what the initial promise settled to. You can explicitly check if the return value from the onFinally handler is a promise, but Promise.resolve() already does that for you and saves you several if statements. You also need to make sure you track the value or error the initial promise settled to, and make sure the returned promise from finally() either fulfills to the initial resolved value res, or rethrow the initial rejected error err.

Moving On

The Promise.prototype.finally() function is one of 8 stage 4 TC39 proposals at the time of this writing, which means finally() and 7 other new core language features are coming to Node.js. The finally() function is one of the most exciting of the 8 new features, because it promises to make cleaning up after async operations much cleaner. For example, below is code that I have running in production right now that desperately needs finally() for releasing a lock after a function is done executing.

Confused by promise chains? Async/await is the best way to compose promises in Node.js. Await handles promise rejections for you, so unhandled promise rejections go away. My new ebook, Mastering Async/Await, is designed to give you an integrated understanding of async/await fundamentals and how async/await fits in the JavaScript ecosystem in a few hours. Get your copy!

Found a typo or error? Open up a pull request! This post is available as markdown on Github
comments powered by Disqus