Wrangling Promises in Node.js: 3 Misconceptions Resolved
ES6 (i.e. proper Javascript) isn't the first time Promises were introduced, but formally supports them now. Believe me, you want to start using Promises in your Node.js scripts, but if you're new to the pattern, it can be tricky to get your head around. That's what I hope to help you do in the next 5 minutes.
What Are Javascript/ES6 Promises?
My way of explaining it is that Promises are chaining pattern, a convention that helps decouple your code blocks from your execution pattern. Promises can dramatically improve your approach to asynchronous programming (such as how Node.js 8+ prefers) and simplify your callbacks by helping you express them in a linear fashion.
[caption id="attachment_762" align="alignnone" width="801"] from Promise (object) on MDN web docs[/caption]
The really easy thing about them is that a Promise ends in either of two conditions:
- Become fulfilled by a value
- Become rejected with an exception
Consider the following code example:
var fetchJSON = function(url) { return new Promise((resolve, reject) => { request({ // synchronous call wrapped in Promise url: url, json: true }, function (error, response, body) { if(!error && response.statusCode === 200) resolve(body); // transformed to an object via 'json: true' above else reject(error ? error : new Error('Was not 200 OK: ' + body)); }) }); }
In the above example, the 'fetchJSON' function returns a Promise, not the result of executing the request. Expressing things this way allows us to execute the code immediately, or as part of an asynchronous chain, such as:
// begins to execute immediately, but results come back asynchronously fetchJSON('http://paulsbruce.io/wp-json/wp/v2/tags') .then(jsonObj => { console.info(jsonObj.map(it => { // print the array of values return jp.value(it,"$..name"); // for each tag, value is 'name' })); }) .catch(error => { console.error(error); }); console.log('after fetchJSON call')
What's the alternative? Well...I hesitate to show you (the interweb loves to copy and paste) because we would have to:
- express every asynchronous action as a callback function (which is bulky)
- indent/embed blocks in a recursive step pattern
- chain commands by calling the next function from our executing function
So far, I've made a career of learning how to stand up and say 'I will not build that'. We should do that more often #facebook and you should read this.
The 3 Misconceptions You Want to Immediately Overcome
Amongst many I had while learning to use Promises, these are the top three I and most others often struggle to overcome:
- You can't mix synchronous/callback-oriented code with Promise-based code
- It's okay to ignore catching errors because it's optional to the Promise chain
- There's no way to join parallel executing paths of asynchronous Promises
I focus on these 3 misconceptions because when they're not in your head, you can focus on the simplicity of Promise code. When writing, ask yourself: "is the code I just wrote elegant?" If the answer is no, chances are you're getting hung up on a misconception.
Mixing Synchronous/Callback-Oriented Code with Promises
You CAN inject legacy synchronous code (code that doesn't emit Promises), but you have to handle Promise-related tie-ins manually. The code example in the last section does exactly that with the 'request' function. However you DO have to wrap it in a function/lambda that eventually calls the 'resolve' or 'reject' handlers.
For instance, in a recent integration to Twitter, their 'stream' function does not return a Promise and only provides a callback mechanism, so I wrapped it to resolve or reject:
async function createTwitterStream(ctx) { return new Promise(function(resolve, reject) { try { client.stream("user", { }, function(stream) { resolve(stream); stream.on("data", function(event) { q.push(function() { onTwitterEvent(event, ctx); }); }); stream.on("error", function(error) { console.error(error) throw error; }); }); } catch(err) { reject(err); } }) }
I decided to 'Promisify' this functionality because I wanted to wrap this logic in a Promise-based retry mechanism so that if the initial stream negotiation failed, it would only fail out of the entire script when multiple attempts failed. I opted to pull in the 'promise-retry' package from npmjs. Simplified the calling code dramatically:
// retry twitter stream up to 5 times promiseRetry(function (retry, number) { console.info('attempt number', number); return createTwitterStream(ctx) .catch(function(err) { console.log(err) if(number <= 5) retry(err); throw err }); }) .catch(err => { console.log('Epic failure to establish Twitter stream after multiple attempts.') throw err; });
Can you see how powerful Promises are now? Imagine how coupled the retry code would be with the stream initialization logic. Again, not going to show you what it looked like before for fear of the copy-n-paste police.
Don't Ignore Error Catching Simply Because the Code Validates!
At first, as I was re-writing blocks of code to Promise-savvy statements, I was getting a lot of these errors:
The problem was that I didn't have '.catch' statements anywhere in the Promise chain. Node.js was interpreting the code as valid until runtime when the error occurred. Bad. Really bad of me. Glad that Node 8 was warning me.
You don't have to write '.catch' after every Promise, particularly if you're returning Promises through functions, so long as the error is handled in at least one place up the Promise chain hierarchy. The Promise model provides you granularity on which errors you want to bubble up.
For instance, in the above code, I DON'T bubble up individual event/tweet errors, but I DO allow stream initialization errors to bubble up to the calling retry code. I can also selectively extend the individual stream event errors to become a bigger problem if the message back from twitter is something like '420 Enhance Your Calm' which essentially means "back the fuck off, asshole".
You CAN Join/Wait for Parallel Executing Promises
The Promise chain lets us string together as many sequential steps as we want via the '.then' handler. But what about waiting for parallel threads of code?
Using the 'Promise.all' function, we can execute separate Promises asynchronously in parallel to each other, but wait in a parent async function by prefixing with the 'await' statement. For example:
await Promise.all([ loadKeywords(ctx) .then((kwds) => { ctx.keywords = kwds; console.debug("Keywords: " + ctx.keywords); }) , loadFriendlies(ctx) .then((frnds) => { ctx.friendlies = frnds; console.debug("Friendlies: " + ctx.friendlies); }) ]); console.debug("Initialization complete. Processing events.");
Within an async function, the above code will wait for both Promises to complete before printing the final statement to the console.
I can tell, now you're really getting the power of decoupling code expression from code execution. I told you that you'd want to start using Promises. As such, I suggest reading up on the links at the end of the article.
Hidden Lesson: Don't Bury Your Head in the Sand!
My takeaway from all this learning is that I should have been applying lessons learned in my Java 7 work to other areas like Node.js. Promises isn't a new idea (i.e. Java Futures, Async C#). If a pattern emerges in one language or framework, it's very likely to already exist in others. If not, find people and contribute to the solution yourself.
If you run into issues, ping me up on Twitter or LinkedIn, and I'll do my best to help in a timely manner.
More reading: