Deferred (promise) pattern
webduvet
Posted on April 8, 2023
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)
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;
}
The new object contains promise
and the handlers to reject
or resolve
it.
{ promise, resolve, reject }
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')
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()
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
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.
Posted on April 8, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 27, 2024
November 6, 2024