diff --git a/README.md b/README.md index 63edff7..e5a5cd4 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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. @@ -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 diff --git a/bower.json b/bower.json index 113cb21..48d5537 100644 --- a/bower.json +++ b/bower.json @@ -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", diff --git a/lib/pacta.js b/lib/pacta.js index cf711eb..7076ca9 100644 --- a/lib/pacta.js +++ b/lib/pacta.js @@ -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) { @@ -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); + } + }); }); } @@ -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); + } + }); }); } @@ -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(); @@ -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); } @@ -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); } @@ -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); diff --git a/package.json b/package.json index 61a4b42..ec77dc6 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/test/pacta_adapter.js b/test/pacta_adapter.js index b61d9fa..1fa9dcf 100644 --- a/test/pacta_adapter.js +++ b/test/pacta_adapter.js @@ -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) { diff --git a/test/pacta_test.js b/test/pacta_test.js index 965075d..e9af678 100644 --- a/test/pacta_test.js +++ b/test/pacta_test.js @@ -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 () { @@ -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 () { @@ -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 () { @@ -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;