These five libraries represent the pre-ES6 era of asynchronous JavaScript, offering Promise implementations and extensions before native support arrived. bluebird, q, when, and rsvp provided feature-rich APIs for complex async flows, while lie focused on being a lightweight polyfill. In modern development, native Promises and async/await have replaced most of these tools, leaving them primarily relevant for maintaining legacy systems or specific polyfill needs.
Before ES6 introduced native Promises, developers relied on third-party libraries to handle asynchronous control flow. Packages like bluebird, q, rsvp, when, and lie filled this gap with varying approaches to API design, performance, and feature sets. Today, understanding these tools is less about choosing one for new work and more about maintaining legacy systems or understanding the history of async JavaScript. Let's examine how they differ in practice.
Before diving into code, you must know the current state of these packages. Most are no longer suitable for new development.
q: Officially deprecated. The repository is archived.when: Repository archived. No longer maintained.rsvp: Legacy status. Primarily tied to older Ember.js versions.bluebird: Maintenance mode. Feature-complete but native Promises are preferred.lie: Polyfill only. Useful only for very old environments.π‘ Rule of Thumb: If you are starting a new project, use native Promises. Only include these packages if you are forced to support old code or browsers without ES6.
One of the biggest advantages these libraries offered over early native Promises was helper methods for running multiple tasks. Native Promises now have Promise.all and Promise.allSettled, but libraries like bluebird and when offered mapped concurrency.
bluebird provides a powerful Promise.map that limits concurrency.
const Promise = require('bluebird');
const urls = ['/api/1', '/api/2', '/api/3'];
// Concurrency limit of 2
Promise.map(urls, fetchUrl, { concurrency: 2 })
.then(results => console.log(results));
q relies on Q.all for parallel execution but lacks built-in concurrency limiting.
const Q = require('q');
const tasks = [fetchUrl('/api/1'), fetchUrl('/api/2')];
Q.all(tasks)
.then(results => console.log(results));
when offers when.map similar to Bluebird.
const when = require('when');
const urls = ['/api/1', '/api/2'];
when.map(urls, fetchUrl)
.then(results => console.log(results));
rsvp uses RSVP.all for parallel execution.
const RSVP = require('rsvp');
const tasks = [fetchUrl('/api/1'), fetchUrl('/api/2')];
RSVP.all(tasks)
.then(results => console.log(results));
lie implements the standard spec only. You must use native-style Promise.all.
require('lie'); // Polyfills global Promise
const tasks = [fetchUrl('/api/1'), fetchUrl('/api/2')];
Promise.all(tasks)
.then(results => console.log(results));
Error handling strategies varied significantly. Some libraries encouraged explicit deferred objects, while others pushed for chaining.
bluebird supports robust error filtering and timeouts.
const Promise = require('bluebird');
Promise.timeoutOperation()
.timeout(5000)
.catch(Promise.TimeoutError, err => {
console.log('Operation took too long');
});
q uses .fail for error handling and often relies on deferred objects.
const Q = require('q');
const deferred = Q.defer();
// ... async work ...
// deferred.resolve(data);
// deferred.reject(error);
deferred.promise.fail(err => {
console.error('Failed:', err);
});
when uses .otherwise for catching errors.
const when = require('when');
when.resolve().then(() => {
throw new Error('Oops');
}).otherwise(err => {
console.error('Caught:', err);
});
rsvp uses standard .catch similar to native Promises.
const RSVP = require('rsvp');
new RSVP.Promise((resolve, reject) => {
// ... work ...
}).catch(err => {
console.error('Error:', err);
});
lie follows the standard spec exactly.
require('lie');
new Promise((resolve, reject) => {
// ... work ...
}).catch(err => {
console.error('Error:', err);
});
A key distinction is whether the package replaces the global Promise or adds a new object with extra tools.
bluebird replaces or extends the global Promise with many extra methods.
const Promise = require('bluebird');
// Adds methods like .delay, .cancel, .map to Promise prototype
Promise.delay(1000).then(() => console.log('Done'));
q creates a separate Q object, leaving global Promise untouched.
const Q = require('q');
// Uses Q object explicitly
Q.fcall(() => 123).then(console.log);
when exports a function and namespace for utilities.
const when = require('when');
// Uses when() function or when.promise()
when.resolve(123).then(console.log);
rsvp exports an RSVP namespace.
const RSVP = require('rsvp');
// Uses RSVP.Promise
new RSVP.Promise(resolve => resolve(123));
lie Polyfills the global Promise constructor only.
require('lie');
// Just makes global Promise work in old IE
new Promise(resolve => resolve(123));
| Feature | bluebird | lie | q | rsvp | when |
|---|---|---|---|---|---|
| Status | Maintenance | Polyfill | Deprecated | Legacy | Archived |
| Extra Utils | β Extensive | β None | β Moderate | β Some | β Moderate |
| Concurrency | β
map w/ limit | β Native only | β all only | β all only | β
map |
| Timeouts | β Built-in | β Manual | β Manual | β Manual | β Built-in |
| Global Polyfill | β Optional | β Yes | β No | β No | β No |
The landscape of asynchronous JavaScript has stabilized around the ES6 standard. For any new architecture, native Promises and async/await are the correct choice. They are built into the runtime, require no dependencies, and are optimized by engine vendors.
bluebird remains the only viable option if you need advanced features like cancellation or concurrency limiting in a Node.js environment, but even then, consider modern alternatives like p-limit. lie has a narrow use case for polyfilling very old browsers without a build pipeline. q, rsvp, and when should be treated as legacy debt β maintain them if you must, but plan their removal.
Bottom Line: Do not introduce these dependencies into new codebases. Use native Promises for clarity, performance, and long-term maintainability.
Choose lie if you need a minimal Promise polyfill for very old browsers that do not support ES6 and you cannot use a build step to transpile code. It is strictly spec-compliant without extra features, making it safe for environments where bundle size is critical. Do not use it in modern environments where native Promises are available.
Choose bluebird only if you are maintaining a legacy Node.js codebase that relies on its specific extensions like cancellation, timeouts, or resource mapping. It is the most feature-complete option among the group, but new projects should use native Promises instead. Avoid adding it as a dependency for greenfield development due to the maintenance overhead.
Do not choose q for any new work as it is officially deprecated and no longer maintained. Only interact with this library if you are refactoring an older codebase that heavily depends on its deferred pattern or progress callbacks. Plan to migrate away from it to native Promises as soon as possible to reduce technical debt.
Choose rsvp only if you are working within an older Ember.js application that has not yet migrated to native Promises. It offers a solid implementation with some helpers like hash, but it is considered legacy technology in the broader ecosystem. New projects should avoid it in favor of standard JavaScript features.
Do not choose when for new projects as the repository is archived and the package is no longer updated. It was once known for performance, but native engines have surpassed it, and it lacks modern safety guarantees. Use it only when maintaining specific legacy integrations that cannot be easily rewritten.
[](https://travis-ci.org/calvinmetcalf/lie)
lie is a small, performant promise library implementing the Promises/A+ spec (Version 1.1).
Originally a fork of Ruben Verborgh's promiscuous, with version 2.6 it became a fork of ayepromise by Chris Burgmer.
npm install lie
var Promise = require('lie');
// or use the pollyfill
require('lie/polyfill');
Either use it with browserify (recommended) or grab one of the files from the dist folder:
Implements the standard ES6 api:
new Promise(function(resolve, reject){
doSomething(function(err, result) {
if (err) {
reject(err);
} else {
resolve(result);
}
});
}).then(function (value) {
//on success
}, function (reason) {
//on error
}).catch(function (reason) {
//shortcut for error handling
});
Promise.all([
//array of promises or values
]).then(function ([/* array of results */]));
Promise.race([
//array of promises or values
]);
// either resolves or rejects depending on the first value to do so
In Node.js, lie emits an unhandledRejection event when a rejected promise isn't caught, in line with how io.js does it. This allows it to act as a promise shim in both Node.js and the browser.