DEV Community

webduvet
webduvet

Posted on

Deferred (promise) pattern

Deferred pattern is one of the most commonly used design patterns in JavaScript world and in programming in general. It postpones the action to a later stage for whatever reason, but mostly because the expected value is not available yet.

Promise falls nicely to this category. It boxes the future value and provides a way to unbox it whether it realizes (then) or fails (catch). The basic usage reveals how the Promise pattern does work:

const promise = new Promise(function(resolve, reject) {
    asyncCall(function(err, result) {
        if (err) {
            reject(err);
        } else {
            resolve(result);
        }
    })
})

promise.then(successHandler).catch(rejectHandler)
Enter fullscreen mode Exit fullscreen mode

New Promise object is constructed and the resolver function is called some time in the constructor. From inside resolver function the asynchronous call is triggered and once the result comes back the status of the Promise changes from pending to settled by calling reject or resolved. At that point any callback provided by calling .than() pipe is executed in the same order how they were registered.

From the above is evident that the programmer or user has limited control when the promise settlement start to happen. Calling the constructor makes the things moving right away. Of course unless we inject some handlers from calling scope or we close over the provided resolve and reject levers.

const makeDeferred = () => {
    const deferred = {};
    deferred.promise = new Promise((resolve, reject) => {
        deferred.resolve = resolve;
        deferred.reject = reject;
    });
    return deferred;
}
Enter fullscreen mode Exit fullscreen mode

The new object contains promise and the handlers to reject or resolve it.

{ promise, resolve, reject }
Enter fullscreen mode Exit fullscreen mode

And that's it. In the simple form and shape.

Deferred as subclass of Promise

The disadvantage if this approach is hacky implementation. The Promise object needs to be thenable and as per specification it needs to return the Promise object instantiated from the same class as the calling object. This means internally the constructor is called with standard resolver method which we need to support. Hence the check for the resolver argument. Also it is going against type definition where Promise expects resolver function in the constructor. Secondly we are robbing ourselves from any potential native performance optimization regarding Promise class or it's instance.

class DeferredPromise extends Promise {
    constructor(resolver) {
        const that = {};
        super(function(resolve, reject) {
            Object.assign(that, {resolve, reject})
        });
        Object.assign(this, that)
        if (resolver) {
            resolver(this.resolve, this.reject)
        }
    }
}

var deferred = new DeferredPromise();

deferred.then(doStuffWithValue)

deferred.resolve('Hello World')
Enter fullscreen mode Exit fullscreen mode

Although this is not recommended, it might be sometimes useful if you have existing middle-ware pipeline working with promise objects and you can get away with dodgy typing.

Deferred as result of a factory function

Building on the original principle, much cleaner and recommended approach is to user factory method

export function Defer(workload) {
    let _reject, _resolve;

    const _p = new Promise(function (resolve, reject) {
        _resolve = resolve;
        _reject = reject;
    })

    const trigger = (function (workload) {
        let _result = undefined;

        return function() {
            if (_result) {
                return _result;
            }
            return _result = workload()
                .then(data => {
                    _resolve(data)
                })
                .catch(reason => {
                    _reject(reason)
                })
        }
    })(workload)

    return {
        get promise() { return _p },
        trigger,
    }
}


var deferred = Defer(() => http.get(url))

// in some class or API expecting Promise value
deferred.promise.then(doStuffWithValue)

// some later stage in app
deferred.trigger()
Enter fullscreen mode Exit fullscreen mode

This implementation takes the async function returning a Promise and returns the new Promise object wrapping the future promise object returned from the async function and the handler to allow as to trigger the async function in the future.

We can see that the _reject and _resolve variables are closed over and the resolve resp reject handlers from the Promise implementation are assigned.
trigger method implements simple caching in case it gets called multiple times.

Wrap up

If anybody find this useful I wrapped all of the above in the platform agnostic npm package, which can be installed by

$ npm install deferable
Enter fullscreen mode Exit fullscreen mode

Or it can be found on github/webduvet/deferable.git. It offers both the Class flavor as well as the Factory flavor.

Deferred Promise pattern has many use cases e.g. api call throttling, user input de-bounce, user action timeout etc.

In this the next article I elaborate one particular use case of Deferred Promise pattern - throttling the async calls.

Top comments (0)