Exploring Asynchronous Promise-Based Workflows In AngularJS
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.
Good points, understanding how the rejected promise gets handled by the first catch function but then becomes good again is very important.
I call these promise paths, but overall the idea is the same http://bahmutov.calepin.co/promise-paths.html
Promises are complex! Here are two articles that I found particularly helpful:
Gleb Bahmutov - http://bahmutov.calepin.co/error-handling-in-promises.html
Tao Of Code - http://taoofcode.net/promise-anti-patterns/
This latter one has been particularly helpful. I've had that baby beast bookmarked for a long time!
Ha ha, small world - I was *just* posting a comment here with a link to another one of your blog posts :D
The node community doesn't use callback hell. Rather, node style callbacks (err then value) are a low level construct that stay unopinionated and can be built upon. Both Q and bluebird can convert those into promises. Now that io.js ships with both promises and generators you can also start using those to do async/await style callback-less asynchronous code (see ES7 async method proposal, or if you want to use it today without transpiler, co.js or bluebird coroutines).
Promises are awesome, but they are just the middle ware. Callbacks are the low level constructs. Async method programming is the way to go once its accessible.
Although I think it could be improved if you use pure asynchronous services as $http (mocked) or $timeout. In this way, you could show how to use the deferred object $q.defer() and how to handle the promise while still unresolved (before being resolved/rejected). These parts can't be seen now.
Thanks for sharing!
I've heard really positive things about Bluebird, but have not looked into it personally. I can imagine, though, that so much of the Node.js core is based on callbacks that trying to shoe-horn promises on top of that could get a little funky. To date, all of my Node.js experience (which is primarily R&D on my localhost) has used callbacks.
I'm definitely a fan of asynchronous processing. But, have become quite smitten with Promises as the callback sugar. Just the other day, I was reading some code that was like:
get something from MongoDB
. . . get something else from MongoDB
. . . . . . save something to MongoDB
. . . . . . . . . redirect user to detail page.
... and, I was reading that in an eBook - so, you can imagine that by the time I got to that inner-most callback, I was getting like 1-2 words on a line before it wrapped :D
The author of the eBook did mention that Node.js has a super popular module called "Async", which looks like it was trying to tame the core callback approach with some parallel features. But, having not seen it before, it was a bit hard to follow - lots of "next()" callbacks being passed around.
So much to learn!
Ah, good point. The demo definitely glosses over the asynchronous nature of remote-HTTP requests. But, the great thing about Promises is that they are always asynchronous! So, whether its a "next tick" under the hood, or a 5-second network latency, the logic for consuming the Promise shouldn't really change much at all.
If I were to replace things with a $timeout, it might look something like this:
Hope that makes things a bit more clear. You can see that it adds a bit more verbosity, which is why I just went with $q.when() and $q.reject().
Thanks for the post!
I am currently working on a "pendingRequests" service to track and to cancel all pending requests from all controllers. It uses a similar process to canceling promises as you have written about in your blog posts. The biggest difference is that I configure $httpProvider to add a promise to each http request and use my service to keep track of each pending request (add/remove requests to tracking array). Calling pendingRequests.cancelAll() loops through all the promises for pending requests and resolves them.
Do you think this is the best way to do this or is there additional cleanup I should do after I resolve the promises?