Skip to content

Commit

Permalink
Promise.prototype.finally [fixes #232]
Browse files Browse the repository at this point in the history
  • Loading branch information
stefanpenner committed Sep 12, 2016
1 parent e25a046 commit d5b4735
Show file tree
Hide file tree
Showing 2 changed files with 188 additions and 0 deletions.
46 changes: 46 additions & 0 deletions lib/es6-promise/promise.js
Original file line number Diff line number Diff line change
Expand Up @@ -379,5 +379,51 @@ Promise.prototype = {
*/
catch(onRejection) {
return this.then(null, onRejection);
},

/**
`finally` will be invoked regardless of the promise's fate just as native
try/catch/finally behaves
Synchronous example:
```js
findAuthor() {
if (Math.random() > 0.5) {
throw new Error();
}
return new Author();
}
try {
return findAuthor(); // succeed or fail
} catch(error) {
return findOtherAuther();
} finally {
// always runs
// doesn't affect the return value
}
```
Asynchronous example:
```js
findAuthor().catch(function(reason){
return findOtherAuther();
}).finally(function(){
// author was either found, or not
});
```
@method finally
@param {Function} callback
@return {Promise}
*/
finally(callback) {
let promise = this;
let constructor = promise.constructor;

return promise.then(value => constructor.resolve(callback()).then(() => value),
reason => constructor.resolve(callback()).then(() => { throw reason; }));
}
};
142 changes: 142 additions & 0 deletions test/extension-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1236,3 +1236,145 @@ describe("Thenables should not be able to run code during assimilation", functio
assert.strictEqual(thenCalled, false);
});
});

describe('Promise.prototype.finally', function() {
describe("native finally behaviour", function() {
describe("no value is passed in", function() {
it("does not provide a value to the finally code", function(done) {
var fulfillmentValue = 1;
var promise = Promise.resolve(fulfillmentValue);

promise['finally'](function() {
assert.equal(arguments.length, 0);
done();
});
});

it("does not provide a reason to the finally code", function(done) {
var rejectionReason = new Error();
var promise = Promise.reject(rejectionReason);

promise['finally'](function(arg) {
assert.equal(arguments.length, 0);
done();
});
});
});

describe("non-exceptional cases do not affect the result", function(){
it("preserves the original fulfillment value even if the finally callback returns a value", function(done) {
var fulfillmentValue = 1;
var promise = Promise.resolve(fulfillmentValue);

promise['finally'](function() {
return 2;
}).then(function(value) {
assert.equal(fulfillmentValue, value);
done();
});
});

it("preserves the original rejection reason even if the finally callback returns a value", function(done) {
var rejectionReason = new Error();
var promise = Promise.reject(rejectionReason);

promise['finally'](function() {
return 2;
}).then(undefined, function(reason) {
assert.equal(rejectionReason, reason);
done();
});
});
});

describe("exception cases do propogate the failure", function(){
describe("fulfilled promise", function(){
it("propagates changes via throw", function(done) {
var promise = Promise.resolve(1);
var expectedReason = new Error();

promise['finally'](function() {
throw expectedReason;
}).then(undefined, function(reason) {
assert.deepEqual(expectedReason, reason);
done();
});
});

it("propagates changes via returned rejected promise", function(done){
var promise = Promise.resolve(1);
var expectedReason = new Error();

promise['finally'](function() {
return Promise.reject(expectedReason);
}).then(undefined, function(reason) {
assert.deepEqual(expectedReason, reason);
done();
});
});
});

describe("rejected promise", function(){
it("propagates changes via throw", function(done) {
var promise = Promise.reject(1);
var expectedReason = new Error();

promise['finally'](function() {
throw expectedReason;
}).then(undefined, function(reason) {
assert.deepEqual(expectedReason, reason);
done();
});
});

it("propagates changes via returned rejected promise", function(done){
var promise = Promise.reject(1);
var expectedReason = new Error();

promise['finally'](function() {
return Promise.reject(expectedReason);
}).then(undefined, function(reason) {
assert.deepEqual(expectedReason, reason);
done();
});
});
});
});
});

describe("inheritance", function() {
function Subclass (resolver) {
this._promise$constructor(resolver);
}

Subclass.prototype = Object.create(Promise.Promise.prototype);
Subclass.prototype.constructor = Subclass;
Subclass.prototype._promise$constructor = Promise.Promise;

Subclass.resolve = Promise.Promise.resolve;
Subclass.reject = Promise.Promise.reject;
Subclass.all = Promise.Promise.all;

it("preserves correct subclass when chained", function() {
var promise = Subclass.resolve().finally();
assert.ok(promise instanceof Subclass);
assert.equal(promise.constructor, Subclass);
});

it("preserves correct subclass when rejected", function() {
var promise = Subclass.resolve().finally(function() {
throw new Error("OMG");
});
assert.ok(promise instanceof Subclass);
assert.equal(promise.constructor, Subclass);
});

it("preserves correct subclass when someone returns a thenable", function() {
var promise = Subclass.resolve().finally(function() {
return Promise.Promise.resolve(1);
});
assert.ok(promise instanceof Subclass);
assert.equal(promise.constructor, Subclass);
});
});
});

0 comments on commit d5b4735

Please sign in to comment.