My previous post on promises explained how to create a thenable with a custom constructor signature, which is a pretty good way to implement something like:

new NetworkRequest(url).then(process)

Calling then in the above returns a vanilla promise, which is useful most of the time.

But what about custom thens? For example, then could be used to periodically check sanity. We might only want the computation to proceed if the intermediate values pass certain checks, and fail otherwise.

new Paranoid().then(check).then(F).
               then(check).then(G).
	       ...

rewritten to

new Paranoid().then(F).then(G)...

A Validating Promise

Domenic Denicola explained how to extend an es6 promise. I used that snippet as a guide to implement ValidatingPromise below.

validating_promise.js
class ValidatingPromise extends Promise {

    then (onFulfilled, onRejected)
    {
        const $onFulfilled = this.wrap(onFulfilled)
        return super.then($onFulfilled, onRejected).copyContext(this)
    }
    
    copyContext (validatingPromise)
    {
	this.validate = validatingPromise.validate
	this.accumulator = validatingPromise.accumulator
	return this
    }

    wrap (onFulfilled)
    {
	let $onFulfilled = (value) => {
	    const error = this.validate(value)
	    if (error)
		throw error +
	              ' after intermediate values ' +
	              this.accumulator.join(',')

	    this.accumulator.push(value)
	    
	    return onFulfilled(value)
	}

	return $onFulfilled
    }
}

Notes on ValidatingPromise

To thread context through a chain of thens I’ve added a copyContext method. Although super.then returns an instance of ValidatingPromise, its contract with the derived class’s constructor cannot be modified. copyContext handles copying non-native promise state, in this case validator information.

wrap takes care of wrapping onFullfilled with validation logic. It only handles onFullfilled because there is no need to validate an already rejected computation.

It all comes together in then(onFulfilled, onRejected) as follows:

  1. Wrap onFullfilled in validation logic.
  2. Create a new promise using super.then.
  3. Copy the non-native promise state to the new promise using copyContext.
  4. Return the new promise

Using ValidatingPromise

Now we need a gimmick to test this contraption. How about a simple blacklist.

blacklist.js
class Blacklist
{
    constructor (iterable)
    {
	this.validator = this.validate.bind(new Set(iterable))
    }

    validate (value)
    {
	if (this.has(value))
	    return `Found blacklisted value ${value}`
    }

    begin (value)
    {
	return new ValidatingPromise((resolve, reject) => {
	    resolve(value)
	}).copyContext({
	    validate: this.validator,
	    accumulator: []
	})
    }
}

Test the blacklist

test_blacklist.js
let add = n => m => n + m

new Blacklist([4,13,666]).
    begin(10).
    then(add(-5)).
    then(add(6)).
    then(add(2)).
    then(add(9)).
    then(add(3)).
    then(console.log).
    catch(console.log)

Result

$ cat validating_promise.js blacklist.js test_blacklist.js | node
Found blacklisted value 13 after intermediate values 10,5,11

As expected, the computation is rejected upon encountering a blacklisted intermediate value.