Skip to content

Commit

Permalink
Promises/A+ 2.0 compliance and exception handling.
Browse files Browse the repository at this point in the history
Any uncaught exceptions raised during execution of a `map` or
`onRejected` handler will now cause the resulting promise to become
`rejected` with the exception as the `reason`. This makes Pacta's
promises similar to Scala's Futures and Promises.
  • Loading branch information
mudge committed Dec 24, 2013
1 parent 7a6237e commit a428b80
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 44 deletions.
33 changes: 31 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# pacta [![Build Status](https://travis-ci.org/mudge/pacta.png?branch=master)](https://travis-ci.org/mudge/pacta)

```javascript
{ 'pacta': '0.3.0' }
{ 'pacta': '0.4.0' }
```

```shell
Expand Down Expand Up @@ -249,6 +249,9 @@ use side-effects within your given function (e.g. `console.log`) as well as
modifying the value and returning it in order to affect the returning
promise.

Note that any uncaught exceptions during the execution of `f` will result in
the promise being `rejected` with the exception as its `reason`.

### `Promise#then([onFulfilled[, onRejected]])`

```javascript
Expand All @@ -268,7 +271,7 @@ promise.then(function (value) {
```

An implementation of the [Promises/A+ `then`
method](http://promises-aplus.github.io/promises-spec/#the__method), taking an
method](http://promisesaplus.com/#the__method), taking an
optional `onFulfilled` and `onRejected` function to call when the promise is
fulfilled or rejected respectively.

Expand All @@ -288,6 +291,32 @@ p.onRejected(function (reason) {
Identical to [`Promise#map`](#promisemapf) but only executed when a promise is
rejected rather than resolved.

Note that `onRejected` returns a promise itself that is fulfilled by the given
function, `f`. In this way, you can gracefully recover from errors like so:

```javascript
var p = new Promise();
p.reject('Error!');

p.onRejected(function (reason) {
return 'Some safe default';
}).map(console.log);
//=> Logs "Some safe default"
```

Like [`Promise#map`](#promisemapf), any uncaught exceptions within `f` will
result in a `rejected` promise:

```javascript
var p = new Promise();
p.reject('Error!');

p.onRejected(function (reason) {
throw 'Another error!';
}).onRejected(console.log);
//=> Logs "Another error!"
```

### `Promise#concat(p)`

```javascript
Expand Down
2 changes: 1 addition & 1 deletion bower.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"description": "An algebraic, Promises/A+ compliant implementation of Promises.",
"keywords": ["promises", "monad", "functor", "promises-aplus"],
"authors": ["Paul Mucur"],
"version": "0.3.0",
"version": "0.4.0",
"main": "lib/pacta.js",
"devDependencies": {
"mocha": "1.12.0",
Expand Down
111 changes: 79 additions & 32 deletions lib/pacta.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
}('Promise', this, function () {
'use strict';

var nextTick, indexOf, EventEmitter, Promise, thenable, reduce;
var nextTick, indexOf, EventEmitter, Promise, reduce;

if (typeof Array.prototype.reduce !== 'function') {
reduce = function (array, callback, initialValue) {
Expand Down Expand Up @@ -173,11 +173,21 @@

if (this.rejected) {
nextTick(function () {
promise.resolve(f(reason));
try {
promise.resolve(f(reason));
} catch (e) {
promise.reject(e);
}
});
} else {
this.emitter.once('rejected', function (reason) {
promise.resolve(f(reason));
nextTick(function () {
try {
promise.resolve(f(reason));
} catch (e) {
promise.reject(e);
}
});
});
}

Expand All @@ -191,11 +201,21 @@

if (this.resolved) {
nextTick(function () {
promise.resolve(f(value));
try {
promise.resolve(f(value));
} catch (e) {
promise.reject(e);
}
});
} else {
this.emitter.once('resolved', function (x) {
promise.resolve(f(x));
nextTick(function () {
try {
promise.resolve(f(x));
} catch (e) {
promise.reject(e);
}
});
});
}

Expand Down Expand Up @@ -287,12 +307,6 @@
});
};

/* Determine whether a value is "thenable" in Promises/A+ terminology. */
thenable = function (x) {
return x !== null && typeof x === 'object' &&
typeof x.then === 'function';
};

/* Compatibility with the Promises/A+ specification. */
Promise.prototype.then = function (onFulfilled, onRejected) {
var promise = new Promise();
Expand All @@ -301,16 +315,7 @@
this.map(function (x) {
try {
var value = onFulfilled(x);

if (thenable(value)) {
value.then(function (x) {
promise.resolve(x);
}, function (reason) {
promise.reject(reason);
});
} else {
promise.resolve(value);
}
Promise.resolve(promise, value);
} catch (e) {
promise.reject(e);
}
Expand All @@ -324,17 +329,8 @@
if (typeof onRejected === 'function') {
this.onRejected(function (reason) {
try {
reason = onRejected(reason);

if (thenable(reason)) {
reason.then(function (x) {
promise.resolve(x);
}, function (reason) {
promise.reject(reason);
});
} else {
promise.resolve(reason);
}
var x = onRejected(reason);
Promise.resolve(promise, x);
} catch (e) {
promise.reject(e);
}
Expand All @@ -348,6 +344,57 @@
return promise;
};

/* The Promises/A+ Resolution Procedure.
* c.f. http://promisesaplus.com/#the_promise_resolution_procedure
*/
Promise.resolve = function (promise, x) {
var then, called = false;

if (promise === x) {

/* 2.3.1. */
promise.reject(new TypeError('Promises/A+ 2.3.1. If promise and ' +
'x refer to the same object, reject ' +
'promise with a TypeError as the ' +
'reason.'));
} else if (x !== null &&
(typeof x === 'object' || typeof x === 'function')) {

/* 2.3.3. */
try {
then = x.then;

if (typeof then === 'function') {
try {

/* 2.3.3.3. */
then.call(x, function (y) {
if (!called) {
Promise.resolve(promise, y);
called = true;
}
}, function (r) {
if (!called) {
promise.reject(r);
called = true;
}
});
} catch (e) {
if (!called) {
promise.reject(e);
}
}
} else {
promise.resolve(x);
}
} catch (e) {
promise.reject(e);
}
} else {
promise.resolve(x);
}
};

/* of :: a -> Promise a */
Promise.of = function (x) {
return new Promise(x);
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
"homepage": "https://github.com/mudge/pacta",
"author": "Paul Mucur (http://mudge.name)",
"keywords": ["promises", "monad", "functor", "promises-aplus"],
"version": "0.3.0",
"version": "0.4.0",
"main": "./lib/pacta.js",
"dependencies": {},
"devDependencies": {
"mocha": "1.10.0",
"promises-aplus-tests": "1.3.1"
"mocha": "1.16.1",
"promises-aplus-tests": "2.0.3"
},
"scripts": { "test": "mocha" },
"repository": {
Expand Down
4 changes: 2 additions & 2 deletions test/pacta_adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ exports.rejected = function (reason) {
return promise;
};

exports.pending = function () {
exports.deferred = function () {
return {
promise: new Promise(),
fulfill: function (value) {
resolve: function (value) {
this.promise.resolve(value);
},
reject: function (reason) {
Expand Down
59 changes: 55 additions & 4 deletions test/pacta_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,15 @@
p = new Promise();
p.map(function () {
triggered = true;
done();
});

p.resolve(1);

assert.ok(triggered);
/* Wait for a new execution context stack. */
setTimeout(function () {
assert.ok(triggered);
done();
}, 50);
});

it('does nothing to rejected promises', function () {
Expand Down Expand Up @@ -125,11 +128,14 @@
p = new Promise();
p.onRejected(function () {
triggered = true;
done();
});
p.reject('error');

assert.ok(triggered);
/* Wait for a new execution context stack. */
setTimeout(function () {
assert.ok(triggered);
done();
}, 50);
});

it('does not trigger onRejected listeners if already fulfilled', function () {
Expand All @@ -155,6 +161,37 @@
done();
});
});

it('can be used to recover from a rejection', function (done) {
p = new Promise();
p.reject(new TypeError());

p2 = p.onRejected(function () {
assert.equal('rejected', p.state());
return 'Some safe default';
});

p2.map(function (x) {
assert.equal('fulfilled', p2.state());
assert.equal('Some safe default', x);
done();
});
});

it('can chain failures', function (done) {
p = new Promise();
p.reject(new TypeError());

p2 = p.onRejected(function () {
assert.equal('rejected', p.state());
throw new TypeError();
});

p2.onRejected(function () {
assert.equal('rejected', p2.state());
done();
});
});
});

describe('#map', function () {
Expand Down Expand Up @@ -197,6 +234,20 @@
});
});

it('encapsulates exceptions in rejections', function (done) {
var exception = new TypeError();

p4 = p.map(function () {
throw exception;
});

p4.onRejected(function (r) {
assert.equal('rejected', p4.state());
assert.equal(exception, r);
done();
});
});

it('fulfils the identity property of a functor', function (done) {
p.map(function (x) {
return x;
Expand Down

0 comments on commit a428b80

Please sign in to comment.