Background
The TC39 proposal-promise-finally
, which is now part of the ES2018 specification, lists the following key points also paraphrased on MDN to describe exactly what the method does.
promise.finally(func)
is similar topromise.then(func, func)
, but is different in a few critical ways:
- When creating a function inline, you can pass it once, instead of being forced to either declare it twice, or create a variable for it
- A
finally
callback will not receive any argument, since there's no reliable means of determining if the promise was fulfilled or rejected. This use case is for precisely when you do not care about the rejection reason, or the fulfillment value, and so there's no need to provide it.- Unlike
Promise.resolve(2).then(() => {}, () => {})
(which will be resolved withundefined
),Promise.resolve(2).finally(() => {})
will be resolved with2
.- Similarly, unlike
Promise.reject(3).then(() => {}, () => {})
(which will be resolved withundefined
),Promise.reject(3).finally(() => {})
will be rejected with3
.However, please note: a
throw
(or returning a rejected promise) in thefinally
callback will reject the new promise with that rejection reason.
In other words, a concise polyfill using a Promise
implementation that conforms to the Promises/A+ specification is as follows (based on the answers by @Bergi and @PatrickRoberts).
Promise.prototype.finally = {
finally (fn) {
const onFulfilled = () => this;
const onFinally = () => Promise.resolve(fn()).then(onFulfilled);
return this.then(onFinally, onFinally);
}
}.finally;
If we contrast a promise chain using Promise#then
and Promise#finally
with an async function
containing a try...finally
block, we can determine some key differences, which are also mentioned but not elaborated here.
const completions = {
return (label) { return `return from ${label}`; },
throw (label) { throw `throw from ${label}`; }
};
function promise (tryBlock, finallyBlock) {
return Promise.resolve()
.then(() => completions[tryBlock]('try'))
.finally(() => completions[finallyBlock]('finally'));
}
async function async (tryBlock, finallyBlock) {
try { return completions[tryBlock]('try'); }
finally { return completions[finallyBlock]('finally'); }
}
async function test (tryBlock, finallyBlock) {
const onSettled = fn => result => console.log(`${fn}() settled with '${result}'`);
const promiseSettled = onSettled('promise');
const asyncSettled = onSettled('async');
console.log(`testing try ${tryBlock} finally ${finallyBlock}`);
await promise(tryBlock, finallyBlock).then(promiseSettled, promiseSettled);
await async(tryBlock, finallyBlock).then(asyncSettled, asyncSettled);
}
[['return', 'return'], ['return', 'throw'], ['throw', 'return'], ['throw', 'throw']]
.reduce((p, args) => p.then(() => test(...args)), Promise.resolve());
.as-console-wrapper{max-height:100%!important}
This demonstrates that the semantics for the settled state of the resulting promise differ from the analog try...finally
block.
Question
What was the reason for not implementing Promise#finally
such that a special case for a callback that resolved to undefined
using the promise resolution procedure was the only condition for which a resolved finally()
re-adopted the state of the original promise?
Using the following polyfill, the behaviors would match more closely with the analog try...finally
block, except for when the finally
block contains an explicit return;
or return undefined;
statement.
Promise.prototype.finally = {
finally (fn) {
const onFulfilled = value => value === undefined ? this : value;
const onFinally = () => Promise.resolve(fn()).then(onFulfilled);
return this.then(onFinally, onFinally);
}
}.finally;
As a follow-up question, in case the consensus is that the current specification is more palatable than the suggestion above, are there any canonical usages of Promise#finally
that would be more cumbersome to write if it used this instead?