Yesterday I posted shortened promises that has raised questions regarding the benefits of compacting promise-based asynchronous code. A strong opinion (and I partially agree) was: such compact code becomes harder to debug and to make more robust. Consider the original code:
1 | var Q = require('q'); |
I can easily add a check // 1 inside the grabMarkdownFiles
to make sure the passed
folder
argument is a valid string, plus output collected files // 2:
1 | var Q = require('q'); |
I cannot add specific argument checks or logging statements to my shortened version
1 | var glob = require('q').denodeify(require('glob')); |
Whenever I have a trade off between shorter code and easier to understand and use code, I pick the later. But in this case, promise-based code can be made robust using the following tricks.
Faster debugging using full stack traces
You can enable full stack traces in Q
promise library using global option at a slight performance penalty. You should also
write custom print function that prints the exception's stack trace (console.error
does not do this)
1 | var Q = require('q'); |
1 | $ node promises.js |
Long stack traces are a great time savior, especially in longer chains.
Passing value along chain
You don't have to create a promise object just to return a value, as long
as there is original promise to start the chain.
The easiest way is to wrap
the original value in Q(value)
call, that returns a promise that resolves
with the passed value.
If any link function returns a non-promise value, it is similarly
passed along to the next step:
1 | var Q = require('q'); |
output
$ node promises.js
foo
Exit when chain completes
If you need to exit the Nodejs process, queue the exit call onto the chain, otherwise nothing might execute
1 | var Q = require('q'); |
output
$ node promises.js
// nothing!
In this example, the first two links of the chain are synchronous
(returning 'foo', printing value). Still the chain starts execution
only after the current code block finishes. It never finishes, since
process.exit
is the last command. For more ways promise-based chains
can play tricks using event queue execution see my
Concurrency can bite you even in Node blog post.
You can at least execute the first link of the chain by placing
process.exit
on the event queue itself
1 | // same code as above |
output
$ node promises.js
foo
Same result could be achieved (less eficiently) using
defer function. All it does is
set 1ms timeout, then executes the exit
call.
1 | var _ = require('lodash'); |
The best way is to tack the exit call onto the chain itself, and make sure it executes in all cases
1 | // same code as above |
output
$ node promises.js
foo
waited foo
foo
Tapping into chain to print value
I often need to inspect values going into or coming out of links of the
promise chain. Using standard console.log
does not work, since it does not
return a value.
1 | var Q = require('q'); |
$ node promises.js
foo
undefined
One can write an utility function to log and return the value (or all arguments)
1 | var Q = require('q'); |
It can become tiresome to write these functions, so here is a shortcut
1 | var Q = require('q'); |
$ node promises.js
foo
foo
I am using two functions from an excellent lodash library. tap function calls its second (right) argument with the first (left) argument, then returns the first (left) argument.
1 | _.tap(value, interceptor) |
We don't have the first argument yet, it will be whatever the previous promise resolves
with. We only have the second argument console.log
that we want to bind as the
second (right) argument. That is what partialRight
function does
1 | _.partialRight(func, [arg]) |
Together function print
does the following: accepts single argument, calls
console.log
on it, then returns the argument.
Argument checking
I love defensive and even paranoid coding. Taking shortcuts with promises makes checking arguments difficult. But one can still tap into the chain in order to do pre- or post- validation.
My favorite library for checking types and values in JavaScript
is Phil Booth's check-types.
For individual methods, we can apply same method we did to console.log
1 | var verify = require('check-types').verify; |
I like to write additional message with my assertion to provide context and allow easier debugging, so my wrapping is a little more verbose:
1 | var verify = require('check-types').verify; |
Both ways return the original value, so they can tap into the chain. Putting it all together, in combination with error handling tips above, I get
1 | var Q = require('q'); |
1 | $ node promises.js |
Conclusion
Promises are powerful, and can be very concise and expressive. Even when writing very complex asynchronous code, you keep the power to tap into the flow to assert pre- or post- conditions or inspect passed values.