Promises
A promise represents the eventual result of an asynchronous operation. The primary way of interacting with a promise is through its then method, which registers callbacks to receive either a promise’s eventual value or the reason why the promise cannot be fulfilled
A quick Google search will tell you that the two most popular implementations of Promises in NodeJS are the Q and Bluebird libraries.
In this blog I am mainly talking about using NodeJS as an API server to server requests from a MongoDB database using the Mongoose driver.
A Sample API Application
Consider the use case that we are creating APIs for an app which helps its users to book and schedule Sunday football matches among themselves.
We want a function getRegisteredUserNames
which returns the names of all players who have registered this week. We look first at some of the the ways to achieve this and what could be wrong in them.
The Dreaded Deferred
We first take a look at an implementation using Q's defer
The above function getRegisteredUserNames
returns a promise, and would work as it should. We go and execute it, and in the then
callback, we receive the data points. Notice the try-catch
block here. This is added since if the code breaks here, the function calling this method has no way to detect what happened. Calling this function, the data
looks like this -
But what we wanted was a list of names rather than objects. One easy workaround here would be to modify the defer.resolve(data)
, and resolving the modifiedData
.
Now we ask the question - Does this code handle all exceptions? What happens if it does not? Obviously Node will throw an exception. But who's catching that exception? What happens to the promise?
Yes, we need another try-catch
over here, else the promise is never resolved nor rejected.
Ugly? Imagine doing this everywhere in your code, using try-catch
blocks everywhere in all callbacks as you write multiple application logics. I'm sure half of your time and lines of code is spent writing these redundant blocks. Also it makes the code harder to debug and maintain. What can be done?
Separation of Concerns
The problem above is that we are trying to mix application logic into a promise creation utility. Create promise at one place and reuse it elsewhere is the general rule to follow. Also, using the defer
objects is a kind of an anti-pattern here and can be fully avoided by making use of the Promise
constructor provided by the promise library of your choice.
Notice that the Q.Promise (Promise in Bluebird) implementation is a wrapper around our code. Any exceptions thrown by the code are made into rejections with the appropriate error. Here, we separated the application logic away into the callee function, where we perform the manipulations.
Second Argument of Promises
The second argument of promises takes a callback which is executed when the promise rejects
. In most circumstances avoid using this. The .catch()
/ .fail()
method is provided specifically for this purpose and makes life much simpler. Note that this is not part of the Promise A+ guidelines, but is anyways provided by both the promise implementations.
Writing APIs
We now look at how to integrate everything together into a NodeJS API using ExpressJS.
We come back to our original example of creating a football match scheduling service. We now wish to write an API which given a start_date
and end_date
will generate random teams, then schedule the matches, and finally send emails to all the players informing them of the team they belong to.
We break down the problem into the following parts -
- Fetch the list players who registered.
- Fetch emails for each user
- Randomly generate teams
- Send emails to each player mentioning the team the user belongs
Writing the API endpoint in the routes/index.js
file
We now write the controller createMatches
. Remember that since this method will just implement application logic, we should not be creating promises here, rather just manipulating them around. However, there are still chances of typos here and there which might go uncaught, and our promise would just be dangling since there is no appropriate handler. To take care of this, the Bluebird library provides a handy wrapper around the functions - Promise.method. We just need to wrap the any function in this, and it should handle any such issues with the code.
Note that in case you are using ES6, you might want to fall back on using the full function definition instead of the array notation while using Promise.method
.
The implementations of the methods under User
, Registry
and sendEmail
should only do what they are supposed to do. Thus they should merely be wrappers around the existing Mongoose/Mongodb/SMTP
functions, and might use the new Promise
if the underlying API only provides a callback (Bluebird allows to promisify callbacks)
The advantages of following the above style is that even though we are doing a lot of things, our logic is flat. Using multiple then
statements, we know the exact execution order. Any errors in any of the step is propagated forward and is caught at the index.js
file in the .catch()
block. Thus a single place to catch all errors.
Conclusion
Separation of concerns along with manipulating promises is the correct way. This way we utilize the maximum capabilities of the awesome promise libraries provided, making code easier to understand, and thus easier to maintain.
More link to read -
- ES6 Native Promises
- Big list of what can be done with promises
- Writing aggregation queries in MongoDB
Comments Section
Feel free to comment on the post but keep it clean and on topic.
blog comments powered by Disqus