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))
);
};
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.
The following are the proposed additions to the specification (section numbering based on the July 18th, 2014 working draft of ECMA-262 edition 6):
When the finally
method is called with argument onSettled the following steps are taken:
- Let promise be the this value.
- If IsPromise(promise) is false, throw a TypeError exception.
- If IsCallable(onSettled) is false, then return Invoke(promise,
"then"
, (undefined, undefined)). - Let alreadyCalled be a new Record { [[value]]: false }.
- Let onFulfilled be a new built-in function object as defined in Promise.prototype.finally Resolve Function.
- Set the value of onFulfilled's [[AlreadyCalled]] internal slot to alreadyCalled.
- Set the value of onFulfilled's [[Handler]] internal slot to onSettled.
- Let onRejected be a new built-in function object as defined in Promise.prototype.finally Reject Function.
- Set the value of onRejected's [[AlreadyCalled]] internal slot to alreadyCalled.
- Set the value of onRejected's [[Handler]] internal slot to onSettled.
- Return Invoke(promise,
"then"
, (onFulfilled, onRejected)).
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:
- Let alreadyCalled be the value of F's [[AlreadyCalled]] internal slot.
- If alreadyCalled.[[value]] is true, then return undefined.
- Set alreadyCalled.[[value]] to true.
- Let handler be the value of F's [[Handler]] internal slot.
- Let handlerResult be the result of calling the [[Call]] internal method of handler passing undefined as thisArgument and () as argumentList.
- ReturnIfAbrupt(handlerResult).
- Return resolution.
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:
- Let alreadyCalled be the value of F's [[AlreadyCalled]] internal slot.
- If alreadyCalled.[[value]] is true, then return undefined.
- Set alreadyCalled.[[value]] to true.
- Let handler be the value of F's [[Handler]] internal slot.
- Let handlerResult be the result of calling the [[Call]] internal method of handler passing undefined as thisArgument and () as argumentList.
- ReturnIfAbrupt(handlerResult).
- Throw reason.
See domenic/promises-unwrapping#18 for the version @domenic recommends.