While most of the Node.js community uses callbacks [hell] for everything, I have a hard time envisioning any asynchronous workflow without Promises. That said, promises are no walk in the park either; while they do simplify things, wrapping your head around a promise-based workflow can be tricky. Just the other day, I was joking with Kyle Simpson that, no matter what I write about promises, he'd likely be able to point out the problems in my thought process:
Right now, I think about the promise chain like a series of gates that the control flow must go through. Each gate has two openings: one for Resolved state and one for Rejected state. If one of your gates is missing a callback for a particular state, the control flow moves onto the next gate using the same state.
If your gate does have a callback for the particular state, however, the result of the callback will determine how the control flow moves onto the next gate. If you return a non-rejected value, the control flow moves onto the next gate in the Resolved state. If you return a rejected value, the control flow moves onto the next gate in the Rejected state. I've tried to illustrate this in the following graphic:
Each callback can also return a promise. This promise is then used to negotiate the control flow movement onto the next "gate."
To explore this, in an AngularJS context, I tried to create a workflow that includes a series of asynchronous steps:
- Load two different friend records.
- Make sure they are superficially compatible.
- Make sure there is no existing friendship.
- Forge a new friendship.
- Log the event to the metric systems.
Since I'm not using any server-side data, all the data is being mocked. That said, all the data is still being delivered via promises, so the lack of HTTP requests shouldn't actually make a difference.
The most interesting part of this asynchronous Promise control flow is checking to see if the friendship already exists. Since we don't want to create duplicate friendships, a "rejection" of the friendship request is actually a good thing - it means that we can move forward. This is why we are translating the "rejected" state into a "resolved" state when moving onto the "create friendship" step.
You'll also notice that I need to break the friendship-check out into an intermediary promise workflow. I have to do this because the error / rejected handler needs to be isolated in order to properly transform the control flow. If it were part of the main promise chain, it would be invoked if any error occurred, such as a failure to load one of the friends. That said, the intermediary promise chain is easily piped back into the main promise chain - all we have to do is return the intermediary promise and it will automatically be used to resolve the next step.
Promises are insanely powerful. But, they are also quite complex in their simplicity. Hopefully, my mental model is starting to become more accurate. And, hopefully the practice of promises in AngularJS is easily translatable to relevant parts of Node.js.
Want to use code from this post? Check out the license.