
Bluebird: Asynchronous Tool Belt
Asynchrony. Asynchrony never changes. Node.js used asynchrony to get great rps for io operations. TC39 has added promises to the callback hell specification. Finally, we standardized async / await. But asynchrony never changes. Wait a minute it turns blue in the sky? It looks like a bluebird carries in its beak a tool belt for those of us who are tightly hooked on thenable objects and all this asynchronous noodles.
If anyone is unfamiliar, bluebird is a library that implements the functionality of promises for javascript. If you are unlikely to drag it into the client assembly, just like 21Kb gzipped, then you simply do not have the moral right to not use it on the server side. Bluebird is still faster than the native implementation. You can not take the word for it, but download the repository and run benchmarks on the latest version of Node.js (9.xx). You can read more about the benefits in a brief overview of the library's architectural principles .
In addition to happiness, the library provides many methods straight from tomorrow, complementing the basic mechanism of promises. Therefore, I propose to get acquainted with the most interesting of the methods. I hope this encourages you to dive deeper into the documentation, as there is still so much to eat.
Let's start with a fairly light and well-known one, monitoring new features in ECMAScript, namely finally. Exactly the same method is now part of the specification (included in the ES2018 release). Allows you to register a handler that works regardless of the final state of the promise (fullfiled, rejected).
// - 1 -
// after fullfill -> always
Promise.resolve(42)
.then(() => console.log('after fullfill'))
.catch(() => console.log('after reject'))
.finally(() => console.log('always'));
// - 2 -
// after reject -> always
Promise.reject(42)
.then(() => console.log('after fullfill'))
.catch(() => console.log('after reject'))
.finally(() => console.log('always'));
This method, like the good old then and catch, returns a new promise that you can subscribe to. It is important that in the case of transition to the rejected state, the finally handler is not considered to be a successful error handling, so it will continue to propagate to the first catch handler.
// - 1 -
// after fullfill -> always -> a bit later
Promise.resolve(42)
.then(() => console.log('after fullfill'))
.finally(() => console.log('always'))
.then(() => console.log('a bit later'));
// - 2 -
// after reject -> always -> a bit later
Promise.reject(42)
.catch(() => console.log('after reject'))
.finally(() => console.log('always'))
.then(() => console.log('a bit later'));
// - 3 -
// always -> after reject
Promise.reject(42)
.then(() => console.log('after fullfill'))
.finally(() => console.log('always'))
.then(() => console.log('never'))
.catch(() => console.log('after reject'));
And, of course, you can return a promise from the finally handler. The remaining chain will wait for its completion by calling subsequent handlers.
// always -> after 1s
Promise.resolve(42)
.finally(() => {
console.log('always');
return delay(1000);
})
.then(() => console.log('after 1s'));
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
We move on. The guys pumped the catch method - with it you can easily filter the errors that we want to handle:
class DeannonizationError extends Error {}
class BigBrotherWatchingYouError extends Error {}
// - 1 -
// better run
Promise.reject(new DeannonizationError())
.catch(DeannonizationError, () => console.log('better run'))
.catch(BigBrotherWatchingYouError, () => console.log('too late'));
// - 2 -
// too late
Promise.reject(new BigBrotherWatchingYouError())
.catch(DeannonizationError, () => console.log('better run'))
.catch(BigBrotherWatchingYouError, () => console.log('too late'));
// - 3 -
// oh no
Promise.reject(new BigBrotherWatchingYouError())
.catch(DeannonizationError, BigBrotherWatchingYouError, () => console.log('oh no'));
This encourages you to write error handling in a more atomic style with good potential for code reuse. Also, in addition to the prototype, you can use the predicate function:
// predicate
Promise.reject({ code: 42 })
.catch(error => error.code === 42, () => console.log('error 42'));
// shorthand for checking properties
Promise.reject({ code: 42 })
.catch({ code: 42 }, () => console.log('error 42'));
One of the most remarkable methods of the library and it is extremely strange that it is not in the standard is any.
// 42
Promise.any([
Promise.reject(40), // error
Promise.reject(41), // error
Promise.resolve(42), // success
]).then(x => console.log(x));
Allows you to wait for at least one promise from the transferred array to complete. In more detail, the promise created by the any method will go into the fullfiled state when any of the promises goes into this state. The handler in then will get the value from this permitted promise:
// 500
Promise.any([
delay(1000),
delay(500),
delay(700),
]).then(x => console.log(x));
function delay(ms) {
return new Promise(resolve => setTimeout(() => resolve(ms), ms));
}
If all transmitted promises fail, the aggregate promise will also go into the rejected state. The catch handler will receive a special error that combines the reasons for the rejection of all promises. Note that the order of errors depends on the order of their occurrence, and not on the initial order of promises.
// - 1 -
// 40 -> 41 -> 42
Promise.any([
Promise.reject(40),
Promise.reject(41),
Promise.reject(42),
]).catch(error => error.forEach(x => console.log(x)));
// - 2 -
// 500 -> 700 -> 1000
Promise.any([
delayAndReject(1000),
delayAndReject(500),
delayAndReject(700),
]).catch(error => error.forEach(x => console.log(x)));
function delayAndReject(ms) {
return new Promise((resolve, reject) => setTimeout(() => reject(ms), ms));
}
In fact, the any method is a special version of the some method with the count parameter equal to 1. Thus, through some we can explicitly set the conditions for the aggregation promise to transition to the fulfilled state:
// [40, 41]
Promise.some([
Promise.resolve(40),
Promise.resolve(41),
Promise.reject(42),
], 2).then(x => console.log(x));
If you often need to run an asynchronous operation in parallel for each element of the array and then wait for all the results, then this code is familiar to you:
// [1, 2, 3]
const promises = [1, 2, 3].map(x => Promise.resolve(x));
Promise.all(promises)
.then(x => console.log(x));
Bluebird provides us with a shortcut for this:
Promise.map([1, 2, 3], x => Promise.resolve(x))
.then(x => console.log(x));
The only thing you should pay attention to: for a function passed as a mapper, the third parameter instead of an array is its length. Also, the map method has an object of settings passed after the mapper. At the moment, there is only one option - concurrency - controlling how many promises can be run in parallel:
// start of 1000ms timer
// start of 2000ms timer
// end of 1000ms timer
// start of 3000ms timer
// end of 2000ms timer
// end of 3000ms timer
// after 4000ms
Promise.map([1000, 2000, 3000], x => delay(x), { concurrency: 2 })
.then(x => console.log('after 4000ms'));
function delay(ms) {
console.log(`start of ${ms}ms timer`);
return new Promise(resolve => setTimeout(() => {
console.log(`end of ${ms}ms timer`);
resolve();
}, ms));
}
But what happens if you set concurrency to 1? True, promises will be executed sequentially. For this, there is also a shortcut:
// start of 1000ms timer
// end of 1000ms timer
// start of 2000ms timer
// start of 3000ms timer
// end of 2000ms timer
// end of 3000ms timer
// after 6000ms
Promise.mapSeries([1000, 2000, 3000], x => delay(x))
.then(x => console.log('after 6000ms'));
function delay(ms) {
console.log(`start of ${ms}ms timer`);
return new Promise(resolve => setTimeout(() => {
console.log(`end of ${ms}ms timer`);
resolve();
}, ms));
}
Often there is a situation when you need to transfer some intermediate data between the promise handlers within the chain. You can use Promise.all and destructuring for these purposes. Another option would be to use a common context bound to handlers in then and catch using the bind method:
// {x: 42, y: 43}
Promise.resolve(42)
.bind({})
.then(function (x) {
this.x = x;
return Promise.resolve(43);
})
.then(function (y) {
this.y = y;
})
.then(function () {
console.log(this)
});
For cases when a function returning a promise may have synchronous returns, you can use the method utility to resolve them in automatic mode. For example, it will be useful in memoizing asynchronous operations. Unlike try, method returns a higher order function:
Promise.method(semiAsyncFunction)()
.then(x => console.log('I handle both sync and async results', x));
function semiAsyncFunction() {
if (Math.random() > 0.5) {
return 420;
}
return delay(42);
}
function delay(ms) {
return new Promise(resolve => setTimeout(() => resolve(ms), ms));
}
The tap method is useful if you need to insert side effects that do not change data into an existing chain, for example, for logging:
// log 42
// process 42
Promise.resolve(42)
.tap(x => console.log(`log ${x}`))
.then(x => console.log(`process ${x}`));
If the side effect is an asynchronous operation, and it is important to wait for its execution, we habitually return the promise from the handler:
// start logging
// log 42
// process 42
Promise.resolve(42)
.tap(x => asyncLogging(x))
.then(x => console.log(`process ${x}`));
function asyncLogging(x) {
console.log('start logging');
return new Promise(resolve => setTimeout(() => {
console.log(`log ${x}`);
resolve();
}, 1000));
}
There is also a version of the method for errors:
// log error 42
// process error 42
Promise.reject(42)
.tapCatch(x => console.log(`log error ${x}`))
.catch(x => console.log(`process error ${x}`));
Also, as with catch, you can do filtering:
class DeannonizationError extends Error {}
class BigBrotherWatchingYouError extends Error {}
// log deannonimization
// process deannonimization
Promise.reject(new DeannonizationError())
.tapCatch(DeannonizationError, x => console.log('log deannonimization'))
.tapCatch(BigBrotherWatchingYouError, x => console.log('log bbwy'))
.catch(DeannonizationError, () => console.log('process deannonimization'))
.catch(BigBrotherWatchingYouError, () => console.log('process bbwy'));
// log bbwy
// process bbwy
Promise.reject(new BigBrotherWatchingYouError())
.tapCatch(DeannonizationError, x => console.log('log deannonimization'))
.tapCatch(BigBrotherWatchingYouError, x => console.log('log bbwy'))
.catch(DeannonizationError, () => console.log('process deannonimization'))
.catch(BigBrotherWatchingYouError, () => console.log('process bbwy'));
The next feature is under consideration by TC39 as part of a broader topic - the cancellation of asynchronous operations. While it has not yet been delivered, we can be content with little and learn to cancel promises:
Promise.config({ cancellation: true });
const promise = delay(1000)
.then(() => console.log('We will never see this'));
promise.cancel();
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
Some asynchronous operations can be undone. When creating a promise, Bluebird will provide you with a special method for registering a callback called upon cancellation:
Promise.config({ cancellation: true });
const promise = delay(1000)
.then(() => console.log('We will never see this'));
promise.cancel();
function delay(ms) {
return new Promise((resolve, reject, onCancel) => {
const timer = setTimeout(() => {
console.log('and this one too');
resolve();
}, ms);
onCancel(() => clearTimeout(timer));
});
}
It can be useful to set time limits for the operation. Then we have at our disposal the timeout method, which will reject a promise with a TimeoutError error in case of time expiration:
// Time's up!
delay(1000)
.timeout(100)
.then(() => console.log(`We will never see this`))
.catch(Promise.TimeoutError, error => console.log(`Time's up!`))
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
And finally, for mental unloading. If due to insuperable circumstances it is necessary to postpone the start of the asynchronous operation, then the delay method will help:
Promise.delay(1000)
.then(() => console.log(`after 1s`));
On this we should say goodbye. Try the bluebird in your pet projects, and then take it with you to production. See you at JS open spaces!