Before we get started, a word of caution. Understanding Promises could be as difficult as making sense of the Christopher Nolan movie Inception. This article is an attempt to simplify the topic as much as possible. The attempt is to go beyond syntax and show examples on why Promises are indeed needed.
Promises are required if your application performs operations such as database access, file access or fetching user data from the online APIs of Twitter. These operations could take variable amounts of time. Waiting for these calls to complete could stall the application for a few seconds or freeze the UI. The result of the operation could even be an error, after a long wait and another retry attempt. Hence, it is better to make these calls asynchronous (not wait for the results).
JavaScript callbacks
JavaScript began as a browser language, with the primary task of handling user events such as a mouse click. The language has excellent support for event-driven programming. In this language, functions are first class citizens, which means you can use a function where any other primitive data type fits. For example, a function can be passed as an argument. A function can also return a function. A callback is a function that will get executed upon an event. It is common for frontend developers to write many callback functions on mouse events like onClick, onmousedown, etc. The functions (callback) get called upon mouse events.
Another important point to note is that JavaScript is single-threaded. Any operation (function call), which takes time, should be asynchronous.
Lets take an example of a callback.
1. var fs = require(fs); 2. fs.readFile(readme.txt, printfile); 3. function printfile(err, filecontents) { 4. console.log(filecontents.toString()); 5. }
printfile() is a callback function, which would be called after a read operation. One drawback of a callback function is that the flow of execution is different from the sequence of instructions written from top to bottom.
Another drawback of the callback function is called Pyramid of Doom. As we go on nesting multiple callbacks, it becomes difficult to debug. Some nested callbacks might result in an exception, and tracing exceptions is a nightmare.
Promise
Promise is a place holder for a future value. This is because we want to store the results of an asynchronous operation. Promise is an object. The syntax below is used to create a Promise.
The syntax is:
new Promise (function)
Given below is an example of how to create a function that returns a Promise. We have converted an asynchronous fs.readFile() operation into a Promise.
1. function readFile(filename) { 2. let p = new Promise(function(resolve, reject) { 3. fs.readFile(filename, function(err, contents) { 4. if (err) { 5. reject(err); // error case 6. } 7. resolve(contents); 8. }); 9. }); 10. return p; 11. }
Line 2: Creates a new Promise object. The object p gets a value after reading the file.
Line 3: A Promise takes a function. The function in turn takes two functions resolve and reject as parameters. For all successful cases, it executes resolve. For error scenarios, the second parameter function is executed.
Promise states
Promise has states. Initially, when a Promise is created, it is in the pending state. This is the initial state. Once the asynchronous operation completes, the Promise moves to the resolved state. There are two possible states within resolve. These are: fulfilled and rejected. A successful result of a resolved Promise is fulfilled. An error case of a Promise is the rejected state.
Using the readFile() function, we can see how the Promise code looks.
1. #!/usr/local/bin/node 2. use strict; 3. console.log(first line); 4. let fs = require(fs); 5. function readFile(filename) { 6. let p = new Promise(function(resolve, reject) { 7. fs.readFile(filename, function(err, contents) { 8. if (err) { 9. reject(err); // error case 10. } 11. resolve(contents); 12. }); 13. }); 14. return p; 15. } 16. let promise = readFile(readme.txt); 17. 18. promise.then(fulfill, reject); 19. 20. function fulfill (contents) { 21. console.log(contents.toString()); 22. } 23. 24. function reject (err) { 25. console.log(err.message) 26. } 27. console.log(last line);
The output of the above program is shown below:
first line last line README file contents
Line 16: This invoked the function readFile(), which returns a Promise.
Line 18: After the file contents are read, the Promise comes to the resolved state. For a successful file read, fullfill() gets executed. For an error scenario like the file not existing, reject gets executed.
One of the advantages of Promise is chaining, which is done by appending multiple .then() functions. You can also have one error handling function using .catch(). Line 18 can be changed to whats shown below:
promise.then(fulfill).then(process).then(processFurther);
or, what follows:
promise.then(fulfill).then(process).catch(reject);
Promise all() and Promise.race()
There are two more methods that Promise provides. These two are used when you have to iterate through multiple Promises (async operations). Instances of it could be useful when you have to gather content from multiple sources/APIs.
Promise.all([p1, p2, p3]).then(function(value) { console.log(value); }, function(reason) { console.log(reason) });
Promise.all() takes multiple Promises as an array. The first function of .then is executed when all Promises (p1, p2 and p3) are fulfilled. Even if one fails, the second function is called for error handling.
Promise.race() is similar, except that the parent Promise will be fulfilled even if only one of the three Promises are fulfilled. This could be used when fetching dictionary data from multiple sources. The result from whichever source is fetched faster, and is good to display. This method can also be used to timeout after predefined milliseconds.
Promises with arrow functions
In previous articles in this series, we covered another feature of ES6 called arrow functions. They come handy when used in combination with Promises.
The earlier program, from Lines 8 to 16, can be modified to use anonymous functions, as follows:
promise.then(function (contents) { console.log(contents.toString()); }, function (err) { console.log(err.message) }); This can further be enhanced to: promise.then((contents) => { console.log(contents.toString()); }, (err) => { console.log(err.message) });
Useful fetch module
#!/usr/local/bin/node use strict; var fetch = require(node-fetch); fetch(http://api.icndb.com/jokes/random).then(function (res) { return res.json(); }).then(function (json) { console.log(json.value.joke); });
node-fetch is a lightweight module to get the contents of a file or URL. It returns a Promise. The above code is a small node.js program that fetches a joke from the Internet database and prints it on the console.
Support matrix
Promise is supported in the Chrome, Firefox and Edge browsers. In non-browser environments, it is supported in the latest versions of Node and Babel. For detailed support, check the Kangax link in References.
Many popular JavaScript libraries and frameworks have adopted Promise due to its merit. Some popular ones are jQuery, Ember.js, etc. Understanding Promise is essential for the async feature, which will come with future versions of ECMAScript.
References
[1] An excellent detailed article on Promise with code examples:
http://www.codeproject.com/Articles/1079322/Learn-How-to-Make-ES-Promises-with-Executable-Exam
[2] An introduction to JavaScript Promise http://dev.paperlesspost.com/introduction-javascript-promises/205
[3] Promise in JavaScript https://www.youtube.com/watch?v=oa2clhsYIDY
[4] A detailed chapter on Promise in ExploringJS book http://exploringjs.com/es6/ch_promises.html
[5] Kangax support matrix http://kangax.github.io/compat-table/es6/