Asynchronous Adventures in JavaScript: Promises
Why Promises?
In the previous Asynchronous Adventure we discussed the idea/pattern of callbacks in JavaScript. The callback pattern is awesome! One of the drawbacks of the callback pattern is in order to handle sequential async behavior this often resorts in a code which becomes harder to read, debug, and maintain. This is often referred to as callback hell.
One way to manage asynchronous behavior that is often considered to be a leveled up version of the callback pattern is the Promise pattern.
A Brief History
Promises are not a new concept. In fact, they have been around since 1976, when the term was first coined.
In the JavaScript world, in the beginning of 2011, Promise concepts were made popular by jQuery Deferred Objects. Deferred Objects are conceptually similar to Promises, but they do not abide by the current ECMAScript 2015 spec for Promises. These concepts ushered in a new way in JavaScript of managing the potential mess of callbacks in a cleaner, simpler way.
In 2012 Promises were proposed as a spec in order to standardize the way many library authors were using Promises. This also brought more attention to the Promise pattern, and allowed for differing libraries and frameworks to work together and implement the Promise pattern.
Finally Promises were officially accepted into the ECMAScript 2015 spec and has almost been implemented in all of the evergreen browsers and Node.
What is a Promise?
“The Promise object is used for deferred and asynchronous computations. A Promise represents an operation that hasn’t completed yet, but is expected in the future.” — MDN Promise Reference
A promise has 3 states:
- pending
- fulfilled
- rejected
Simply put, a Promise is an object that holds a value that is promised to be given to you at some point in time. One can retrieve the value a Promise is holding by calling the Promises then method. A promise starts out in the pending state, then on success it will be in the fulfilled state, or if an error occurs, it will be in the rejected state.
let promise = fetch('api/give-me-json');promise.then( (response) => {
console.log(response);
});
In this example, the promise returned by the fetch method will eventually contain a response object. When we need to access the value, we call the promises then method passing in an arrow function whose parameter is the value that the promise holds.
What if fetch throws an error?
let promise = fetch('api/give-me-json');promise.then( (response) => {
console.log(response);
}, (err) => {
console.log(err)
});
The difference here is that when we call then on the promise, we pass in another function which is classified as the error handler, and this function receives as it’s parameter passed into it, the error that the fetch function threw. In this example, this error could have been a timeout error, or simply a 404 Not Found error. More on handling errors below.
Creating your own promises
Creating your own promises is rather simple.
In this example, we define a function which returns a new Promise object. This Promise will eventually resolve to a value of 2 after 2000 milliseconds. In order to create this promise, we pass a function into the Promise’s constructor. This function takes in two parameters, resolve and reject. Each of these parameters are functions. To fulfill the Promise, we invoke resolve passing in the value that will be passed into the first parameter of the Promise’s then method, when it is called. Reject works similarly except it is for any errors. It is passed either to the 2nd parameter of the Promise’s then method, or the parameter of the Promise’s catch method (covered shortly below).
Error handling
Chaining .then and .catch
Something that I have not discussed yet, is the ability to chain then calls. This is possible because the then method returns yet another Promise. This allows to control the flow of execution in an easy manner. Let’s take a look.
/* NOTE: readFile, countLines, and errorHandler are all functions which return Promises. */fs.readdir(source)
.then(loadFiles)
.then(countLines)
.catch(errorHandler);
In this example, we call the Node fs (file-system) core module readdir (read directory). We pass in the source that has been predefined somewhere. I am now assuming that the method returns a promise (I know it doesn’t actually). Then after it’s done loading, I want to load the files, then after that’s finished, I want to count the number of lines in all of the files. Lastly if there are any errors in any of the operations, they will be caught and passed into the error handler function. It is worth noting that catch also returns another promise.
Here’s another example, and if you’re confused about Promise.resolve, basically it returns a Promise which resolves to the value passed into it. This is explained in more detail after this example.
Named functions
In this example I am using predefined named functions, and in my opinion this is where Promises really shine. If I used unnamed functions for each of the then methods, the code would be much harder to read, debug, and maintain. I believe this because oftentimes there will be logic in each of these functions, and this part of the code isn’t concerned with the logic of each individual function, it is concerned with the overall flow of execution. So using named functions in this context allows for a greater separation of concerns, and is also simpler to read.
The rest of the Promise methods
The following methods are static, and called off of the global Promise object.
Promise.resolve( value | Promise )
Promise.resolve receives either a value or a Promise, and returns a Promise. If the parameter is a value, the returned promise will resolve to the value passed in, analogous to Promise.reject. But if Promise.resolve receives a Promise, the returned Promise will follow the eventual state of the received Promise object.
Promise.reject( reason )
Promise.reject takes in a reason object, which can be an object such as a String or Error. This static method returns a Promise in the rejected state, and passes the reason into the error handler of a then or a catch called off of the returned Promise.
Promise.all( iterable )
Promise.all takes in something that is iterable like an Array. This iterable object must contain a list of Promise objects. Promise.all returns a Promise which resolves when all of the Promises passed into the method resolve. This allows us to kick off multiple async calls and wait to execute further code until all of the async calls are completed.
Promise.race( iterable )
Promise.race again receives an iterable list of Promises, and returns a Promise. The returned Promise resolves or rejects whichever Promise resolves or rejects first among the Promises which are passed into to Promise.race.
In Summary
Promises provide an awesome interface for building asynchronous execution flows. They provide a more readable alternative to the callback pattern, and they are the foundation with with the upcoming future Async/Await pattern is build upon. In order to properly understand some of the lessons to come, as well as other asynchronous patterns, it is essential to understand how Promises work. Trust me, I promise… (I’m sorry, I couldn’t resist).
More Resources
- You Don’t Know JS (Promise chapter)
- Mozilla Developer Network Docs
- Understanding ES6 (Promise section)
Stay Tuned
Please join me in this series of Asynchronous Adventures where I will be highlighting how to handle asynchronous code in JavaScript from the basic stuff, to the advanced, fancy strategies.
This series covers in-depth analysis (with examples!) of the following JavaScript patterns:
- Understanding the Event Loop
- Callbacks
- Promises (You are here)
- Generators
- Async/Await
Ben Diuguid is currently a student at the University of Florida. He is incredibly passionate about creating things, especially with JavaScript.