CompletablePromise
CompletablePromise allows to create a Promise instance that does not start its resolution upon its declaration.
Table of Contents
Installing
Using npm:
npm install completable-promise
Using bower:
bower install completable-promise
Using yarn:
yarn add completable-promise
Back to top
Usage
A CompletablePromise can be initialized as follows:
const CompletablePromise = require('completable-promise').CompletablePromise;
import { CompletablePromise } from "completable-promise";
const completablePromise = new CompletablePromise();
completablePromise.then(value => {
console.log(value);
}).catch(reason => {
console.error(reason);
});
This kind of promise will remain in pending state until one among resolve or reject methods is explicitly called:
-
resolve will trigger the then transition
completablePromise.resolve('foo');
-
reject will trigger the catch transition
completablePromise.reject('error');
After the first resolve or reject call, future ones will be ignored:
const completablePromise = new CompletablePromise();
completablePromise.then(value => {
console.log(value);
}).catch(reason => {
console.error(reason);
});
completablePromise.resolve('foo');
completablePromise.resolve('bar');
completablePromise.reject('error');
Back to top
CompletablePromise states
CompletablePromise states reflect the Promise states (more info can be found here).
Upon initialization, a CompletablePromise will be in pending state.
const completablePromise = new CompletablePromise();
console.log(completablePromise.getState());
console.log(completablePromise.isPending());
console.log(completablePromise.isSettled());
Upon calling resolve, the state will irreversibly change to fulfilled:
completablePromise.resolve('foo');
console.log(completablePromise.getState());
console.log(completablePromise.isPending());
console.log(completablePromise.isSettled());
console.log(completablePromise.isFulfilled());
console.log(completablePromise.isRejected());
completablePromise.reject('error');
console.log(completablePromise.isFulfilled());
console.log(completablePromise.isRejected());
Upon calling reject, the state will irreversibly change to rejected:
completablePromise.reject('error');
console.log(completablePromise.getState());
console.log(completablePromise.isPending());
console.log(completablePromise.isSettled());
console.log(completablePromise.isFulfilled());
console.log(completablePromise.isRejected());
completablePromise.resolve('foo');
console.log(completablePromise.isFulfilled());
console.log(completablePromise.isRejected());
Back to top
CompletablePromise antipattern solution
When using a CompletablePromise, the following antipattern (where errors should be explicitly handled within a try...catch) can arise:
const completablePromise = new CompletablePromise();
completablePromise.then(value => {
console.log(value);
}).catch(reason => {
console.error(reason);
});
const brokenJsonString = '{"foo":"bar"';
try {
completablePromise.resolve(JSON.parse(brokenJsonString));
} catch (exception) {
}
Such thing does not happen with the classic Promise approach:
const brokenJsonString = '{"foo":"bar"';
const promise = new Promise((resolve, reject) => {
resolve(JSON.parse(brokenJsonString));
});
promise.then(value => {
console.log(value);
}).catch(reason => {
console.error(reason);
});
A solution to this problem is using tryResolve method:
const completablePromise = new CompletablePromise();
completablePromise.then(value => {
console.log(value);
}).catch(reason => {
console.error(reason);
});
const brokenJsonString = '{"foo":"bar"';
completablePromise.tryResolve(() => {
return JSON.parse(brokenJsonString);
});
Back to top
Mixing CompletablePromise and Promise
Sometimes there are situations where the results of more promises need to be aggregated with Promise.all, Promise.allSettled, Promise.any and Promise.race constructs.
For this purpose, the get method allows to retrieve the inner Promise instance of a CompletablePromise:
const completablePromise = new CompletablePromise();
const promise = new Promise((resolve, reject) => {
resolve('bar');
});
Promise.all([completablePromise.get(), promise]).then(values => {
console.log(values)
});
completablePromise.resolve('foo');
Back to top
Examples
Callback to promise
A possible use case of this library is to promisify a function that is based on the callback approach, avoiding the callback hell/pyramid of doom problem.
The following example shows how to prompt multiple times the user for some input. Even if the CompletablePromise approach is a bit more elaborated, its result is surely clearer thanks to the chaining of the promises.
Common setup:
import { createInterface } from "readline";
const readLine = createInterface({
input: process.stdin,
output: process.stdout
});
Callback approach:
readLine.question('Step 1) Insert a value: ', value => {
console.log(value);
readLine.question('Step 2) Insert another value: ', value => {
console.log(value);
readLine.question('Step 3) Insert once again a value: ', value => {
readLine.close();
console.log(value);
});
});
});
CompletablePromise approach:
import { CompletablePromise } from "completable-promise";
function readUserInput(query) {
const completablePromise = new CompletablePromise();
readLine.question(query, value => {
completablePromise.resolve(value);
});
return completablePromise;
}
readUserInput('Step 1) Insert a value: ').then(value => {
console.log(value);
return readUserInput('Step 2) Insert another value: ');
}).then(value => {
console.log(value);
return readUserInput('Step 3) Insert once again a value: ');
}).then(value => {
readLine.close();
console.log(value);
}).catch(reason => console.error(reason));
Back to top
Asynchronous tail recursion
This library can also be used to achieve asynchronous tail recursion. This is useful in situations where an event will happen but it is not known with precision when. For example, you may need to run a command only after a service is ready and a push based approach is not available/possible:
import express from 'express';
const app = express();
app.get('/status', (req, res) => {
res.send('ready')
});
const initializationDelay = Math.floor(Math.random * 20000) + 10000;
setTimeout(() => {
const port = 3000;
console.log(`app listening at http://localhost:${3000}`);
app.listen(port);
}, initializationDelay);
import axios from 'axios';
import { CompletablePromise } from 'completable-promise';
const completablePromise = new CompletablePromise();
const waitDeployment = () => {
console.trace();
axios.get('http://localhost:3000/status').then((response) => {
completablePromise.resolve(response);
}).catch(e => {
console.log('service is not ready, checking once again its state after 2 seconds');
setTimeout(() => waitDeployment(), 2000);
});
}
waitDeployment();
completablePromise.then(result => {
}).catch(reason => {
console.error(reason);
});
In the example above, the stack trace remains constant even though waitDeployment function is recursive.
Back to top
Contributing
Contributions, issues and feature requests are welcome!
Back to top
License
This library is distributed under the MIT license.
Back to top