-1

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 to promise.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 with undefined), Promise.resolve(2).finally(() => {}) will be resolved with 2.
  • Similarly, unlike Promise.reject(3).then(() => {}, () => {}) (which will be resolved with undefined), Promise.reject(3).finally(() => {}) will be rejected with 3.

However, please note: a throw (or returning a rejected promise) in the finally 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?

Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
  • 3
    I think it's a great question. Just... not for Stack Overflow (as it's a question that really only a small, very specific set of people can answer factually). Another Stack Exchange site might be more suitable, such as [Software Engineering](https://softwareengineering.stackexchange.com) – rossipedia May 09 '19 at 17:13
  • Btw, using `async` as a function name is highly confusing, and invalid in strict mode. – Bergi May 09 '19 at 18:22
  • @rossipedia what's funny is that Bergi was the exact person I was hoping to get an answer from, so I guess it worked out :) – Patrick Roberts May 09 '19 at 18:46
  • @Bergi Using `async` as a function name is _not_ invalid in strict mode, otherwise strict mode would not be backwards compatible with ES2016 and earlier, where it was commonplace to use `async` as an identifier (e.g. `caolan/async`). I'll accept that it's confusing though. – Patrick Roberts May 09 '19 at 19:02

1 Answers1

2

This demonstrates that the semantics for the settled state of the resulting promise differ from the analogue try...finally block.

Not really, you just used the "wrong" try/finally syntax in your comparison. Run it again with

async function async (tryBlock, finallyBlock) {
  try { return completions[tryBlock]('try'); }
  finally {        completions[finallyBlock]('finally'); }
//          ^^^^^^ no `return` here
}

and you will see that it is equivalent to .finally().

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?

The proposal gives the following rationale: "Promise#finally will not be able to modify the return value […] - since there is no way to distinguish between a “normal completion” and an early return undefined, the parallel with syntactic finally must have a slight consistency gap."

To give an explicit example, with the try/catch syntax there is a semantic difference between

finally {
}

and

finally {
    return undefined;
}

but the promise method cannot be implemented to distinguish between

.finally(() => {
})

and

.finally(() => {
    return undefined;
})

And no, it simply made no sense to introduce any special casing around undefined. There's always a semantic gap, and fulfilling with a different value is not a common use case anyway. You rarely see any return statements in normal finally blocks either, most people would even consider them a code smell.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • I think you were spot-on to point out that I should be removing the `return` statement in my analog `try...finally` block, and I somewhat agree that modifying the return value of a function in a `finally` block is code-smell. On the other hand, what irks me is that the specification essentially prevents an entire class of possible continuations, while the minor change in semantics I suggested only prevents the continuation of a single value `undefined`, in order to use it for differentiating between the intent of modifying the resolved value vs just following another asynchronous continuation. – Patrick Roberts May 09 '19 at 18:32
  • To clarify what I'm saying, with my change, you have an equivalent for both `finally { return ...; }` and `finally { ...; }`, assuming you don't need an equivalent for `finally { return undefined; }`, whereas with the specification of `Promise#finally`, you only have an equivalent for `finally { ...; }`, and it makes it very cumbersome to create an equivalent for `finally { return ...; }`. – Patrick Roberts May 09 '19 at 18:38
  • 1
    I guess not supporting *any* return values is more consistent than supporting some return values *except* for one. It would lead to weirder edge cases that a code author would need to account for. While the change in semantics might be minor, the standardised semantics are still (a bit) simpler than those you proposed. – Bergi May 09 '19 at 18:40
  • Btw if you absolutely want to have a new return value, you can use `.catch(e => {}).then(() => …)`. – Bergi May 09 '19 at 18:41
  • Your point about "weirder edge cases for code authors" seems a likely answer to why the semantics I suggested were not used in the specification. Thank you for being so responsive to my follow-up. – Patrick Roberts May 09 '19 at 18:43