Skip to content

Instantly share code, notes, and snippets.

@rbuckton
Last active December 18, 2016 15:53
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rbuckton/66918c8491aa335b003c to your computer and use it in GitHub Desktop.
Save rbuckton/66918c8491aa335b003c to your computer and use it in GitHub Desktop.
Proposal for addition of Promise.prototype.finally

NOTE I have yet to update this proposal, but I find @domenic's version here to be a much improved implementation:

Promise.prototype.finally = function (callback) {
    return this.then(
        value => this.constructor.resolve(callback()).then(() => value),
        reason => this.constructor.resolve(callback()).then(() => throw reason)
    );
};

Though I would modify it to the following to remove the cost of the throw:

Promise.prototype.finally = function (callback) {
    return this.then(
        value => this.constructor.resolve(callback()).then(() => value),
        reason => this.constructor.resolve(callback()).then(() => this.constructor.reject(reason))
    );
};

Summary

The Promise constructor as defined by ES6 currently provides valuable shorthand extension over a general "Thenable" promise as defined by Promise/A+ in the catch method on the Promise prototype. This allows for very simple shorthand when writing asynchronous JavaScript in a fashion roughly analogous to try..catch:

// sync
try {
  return value.someMethod();
}
catch (error) {
  // error handling logic ...
}
// async
return promise
  .then(value => {
    return value.someMethod();
  })
  .catch(error => { 
    // error handling logic ... 
  });

Here, catch is simply shorthand for invoking then with undefined as the first argument. This shorthand is valuable to express intent, even though it does not provide any additional capability that could not already be easily expressed. One scenario that is difficult to express with an ES6 Promise is try..finally:

// sync
try {
  value.someMethod();
}
finally {
  // cleanup regardless of normal or abrupt completion
}
// async
var finallyCallback = () => {
  // cleanup regardless of normal or abrupt completion
}

return promise
  .then(value => {
    return value.someMethod()
  })
  .then(result => {
    finallyCallback();
    return result;
  }, error => {
    finallyCallback();
    throw error; // or `return Promise.reject(error)`
  });

As a result, I'd like to propose the addition of a finally method to the Promise prototype in ES6 (if such a change could be fast tracked) or ES7, as in the following example:

// async
return promise
  .then(value => {
    return value.someMethod()
  })
  .finally(() => {
    // cleanup regardless of normal or abrupt completion
  })

The finally method would take in a single onSettled callback that is called when the antecedent Promise is settled. If the onSettled callback terminates abruptly, the new Promise will be rejected with the reason for the abrupt termination. However, if onSettled results in a normal completion the new Promise will be fulfilled or rejected based on the state and result of the antecedent Promise.

Additions to specification

The following are the proposed additions to the specification (section numbering based on the July 18th, 2014 working draft of ECMA-262 edition 6):

25.4.5.5 - Promise.prototype.finally ( onSettled )

When the finally method is called with argument onSettled the following steps are taken:

  1. Let promise be the this value.
  2. If IsPromise(promise) is false, throw a TypeError exception.
  3. If IsCallable(onSettled) is false, then return Invoke(promise, "then", (undefined, undefined)).
  4. Let alreadyCalled be a new Record { [[value]]: false }.
  5. Let onFulfilled be a new built-in function object as defined in Promise.prototype.finally Resolve Function.
  6. Set the value of onFulfilled's [[AlreadyCalled]] internal slot to alreadyCalled.
  7. Set the value of onFulfilled's [[Handler]] internal slot to onSettled.
  8. Let onRejected be a new built-in function object as defined in Promise.prototype.finally Reject Function.
  9. Set the value of onRejected's [[AlreadyCalled]] internal slot to alreadyCalled.
  10. Set the value of onRejected's [[Handler]] internal slot to onSettled.
  11. Return Invoke(promise, "then", (onFulfilled, onRejected)).

25.4.5.6 - Promise.prototype.finally Resolve Function.

A Promise.prototype.finally resolve function is an anonymous built-in function that is used to resolve a Promise with its antecedent's resolved value after executing a callback. Each Promise.prototype.finally resolve function has [[Handler]] and [[AlreadyCalled]] internal slots.

When a Promise.prototype.Finally resolve function F is called with argument resolution, the following steps are taken:

  1. Let alreadyCalled be the value of F's [[AlreadyCalled]] internal slot.
  2. If alreadyCalled.[[value]] is true, then return undefined.
  3. Set alreadyCalled.[[value]] to true.
  4. Let handler be the value of F's [[Handler]] internal slot.
  5. Let handlerResult be the result of calling the [[Call]] internal method of handler passing undefined as thisArgument and () as argumentList.
  6. ReturnIfAbrupt(handlerResult).
  7. Return resolution.

25.4.5.7 - Promise.prototype.finally Reject Function.

A Promise.prototype.finally reject function is an anonymous built-in function that is used to reject a Promise with its antecedent's rejected value after executing a callback. Each Promise.prototype.finally reject function has [[Handler]] and [[AlreadyCalled]] internal slots.

When a Promise.prototype.Finally reject function F is called with argument reason, the following steps are taken:

  1. Let alreadyCalled be the value of F's [[AlreadyCalled]] internal slot.
  2. If alreadyCalled.[[value]] is true, then return undefined.
  3. Set alreadyCalled.[[value]] to true.
  4. Let handler be the value of F's [[Handler]] internal slot.
  5. Let handlerResult be the result of calling the [[Call]] internal method of handler passing undefined as thisArgument and () as argumentList.
  6. ReturnIfAbrupt(handlerResult).
  7. Throw reason.
@rbuckton
Copy link
Author

See domenic/promises-unwrapping#18 for the version @domenic recommends.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment