Promises in ES2015/ES6
Over the years developers have used multiple libraries to introduce support for promises into their projects. Each library came with different APIs that made it hard for developers to transition their projects to a different promise library. The ES2015 language specification has introduced Promises as a first-class feature, thereby defining a standard API and eliminating the need for external dependencies.
Callbacks
Most JavaScript developers have experienced the pyramid of doom with callbacks. Each callback having a side effect and calling the next callback can make the source code hard to read, write, and test.
The following example is designed to show the complexity involved with callbacks. The generateGreeting method uses window.setTimeout to create an asynchronous operation. The method takes a name, callback, and an optional delay. In the case that an invalid delay was passed to generateGreeting, an Error will be passed to the callback. Otherwise, a greeting will be generated using the name parameter and passed to the callback.
function generateGreeting (name, callback, delayMillis) {
const delay = typeof delayMillis !== 'undefined' ? delayMillis : 1000;
if (delay > 1500) {
callback(new Error('Invalid delay!'));
return;
}
setTimeout(() => callback(null, `Hello ${name}!`), delay);
};
function callback(error, greeting) {
if (error) {
console.log(error.message);
}
else {
console.log(greeting);
}
};
generateGreeting('World', callback); // prints 'Hello World!'
generateGreeting('World', callback, 1501); // prints 'Invalid Delay!'
Promises
A Promise is an object that represents an operation that has not completed. The sections below will develop a promise and demonstrate how promises can help asynchronous methods behave more like synchronous methods.
Define Promise
The Promise constructor takes a resolve callback and reject callback as parameters. The resolve callback is called after a successful completion of the asynchronous operation. The results of the operations will be passed into the callback. The reject callback is called after an error and the error passed into the callback.
The generateGreeting method is similar to the previous example, but returns a Promise that wraps the asynchronous operation. After a specified delay, a greeting will be generated and passed to the resolve callback. If an invalid delay is provided, the reject method will be called with an error message.
function generateGreeting(name, delay = 1000) {
return new Promise((resolve, reject) => {
if (delay > 1500) {
reject(new Error('Invalid delay!'));
}
setTimeout(() => resolve(`Hello ${name}!`), delay);
});
}
Successful Promise
After the asynchronous operation has successfully been completed, the promise’s then method will be called with the results of the asynchronous operation.
In the following example, the generateGreeting method is called with ‘Foo’. After a delay, a greeting will be generated (‘Hello Foo!’) and passed to the promise’s then method.
generateGreeting('Foo')
.then((greeting) => console.log(greeting)); // prints 'Hello Foo!'
Failed Promise
A failed asynchronous operation will call the promise’s catch with the reason for failure.
In the following example, the generateGreeting method is called an invalid delay. The invalid delay will cause the operation to fail and the promise’s catch method will be called with the reason ‘Invalid Delay!’.
const invalidDelay = 1501;
generateGreeting('Foo', invalidDelay)
.catch((error) => console.log(error.message)); //prints 'Invalid Delay!'
Chain Promises
The Promise API provides the ability to chain promises. The chaining of promises removes the pyramid of doom and reintroduces language fundamentals such as return and throw.
In the following example, two generateGreeting promises are chained together. Here the result of the first promise (‘Hello Foo!’) is passed to the next promise to generate a new greeting (‘Hello Hello Foo!!’). After the last promise, the results will be printed to the console.
generateGreeting('Foo') // generates 'Hello Foo!'
.then((greeting) => generateGreeting(greeting)) // generates 'Hello Hello Foo!!'
.then((greeting) => console.log(greeting)); // prints 'Hello Hello Foo!!'
Handling Multiple Promises
Promise.all(iterable) will provide a promise for multiple promises. This aggregate promise will be resolved once all the child promises have been resolved or one of the child promises have been rejected. The resolved promise will be passed an array of results.
In the following example, multiple promises are passed to Promise.all. Once all the greeting promises are resolved, the greetings are passed as an array to the promise’s then method and logged to the console.
const promise1 = generateGreeting('Foo');
const promise2 = generateGreeting('Bar');
Promise.all([promise1, promise2])
.then((greetings) => {
greetings.forEach((g) => console.log(g)); // prints 'Hello Foo!' and 'Hello Bar!'
});
Testing Promises
Testing asynchronous methods is often difficult. The tests can be cumbersome to write and read. However, tools such as Mocha, Chai, and Chai Assertions for Promises make testing promises easier.
Setup
The following commands can be used to install the Mocha, Chai, and Chai Assertions for Promises into a project.
npm install mocha --save-dev
npm install chai --save-dev
npm install chai-as-promised --save-dev
A new spec is introduced to test the generateGreeting method. The chai module and chai-as-promised modules are imported to support testing. The chai module is an assertion library to help facilitate testing. The chai-as-promised module is a chai plugin that adds support for testing promises.
import * as chai from 'chai';
const expect = chai.expect;
import chaiAsPromised from 'chai-as-promised';
chai.use(chaiAsPromised);
describe('ES2015 Promises', () => {
...
});
Test a Successful Promise
The test below verifies that the generateGreeting method will return the expected greeting ‘Hello Foo!’’.
It is important to note that the assertion returns a promise. This notifies the mocha test framework to wait until the promise being tested has been resolved or has been rejected in order to complete the test.
it('should provide a greeting', () => {
// When
const promise = generateGreeting('Foo');
// Then
return expect(promise).to.eventually.equal('Hello Foo!');
});
Test a Rejected Promise
The test below verifies that the generateGreeting method will throw an error for an invalid delay. It is similar to the previous test, but it ensures the promise was rejected with the ‘Invalid delay!’.
it('should handle invalid delay', () => {
// Given
const invalidDelay = 1501;
// When
const promise = generateGreeting('Foo', invalidDelay);
// Then
return expect(promise).to.eventually.rejectedWith('Invalid delay!');
});
Conclusion
It would be hard to come up with a reason not to use promises, instead of callback. Promises are easier to read, write, and test. However, a better way is coming soon. The Async Functions proposal for ES7 will provide the ability to write asynchronous operations as if the methods were synchronous operations.
ES2015 made promises a first-class feature of the language. Node and most of the modern browsers support Promises. Unsurprisingly, Internet Explorer still does not provide support for promises even though the Edge browser does support Promises. If IE or legacy browsers need to be supported, a pollyfill or transpiler such as Babel can been used to add support for Promises.