With Node.js version 8 there is finally a truly attractive alternative to good old callbacks.
I was never a fan of promises, and implementing await-async as a library is not pretty. Now when await and async are keywords in JavaScript things change.
The below program demonstrates a simple async function doing IO: ascertainDir. It creates a directory, but if it already exists no error is thrown (if there is already a file with the same name, no error is thrown, and that is a bug but it will do for the purpose of this article).
There are four modes of the program: CALLBACK, PROMISE, AWAIT-LIB and AWAIT-NATIVE. Creating a folder (x) should work. Creating a folder in a nonexisting folder (x/x/x) should fail. Below is the output of the program and as you see the end result is the same for the different asyncronous strategies.
$ node ./await-async.js CB a Done: a $ node ./await-async.js CB a/a/a Done: Error: ENOENT: no such file or directory, mkdir 'a/a/a' $ node ./await-async.js PROMISE b Done: b $ node ./await-async.js PROMISE b/b/b Done: Error: ENOENT: no such file or directory, mkdir 'b/b/b' $ node ./await-async.js AWAIT-LIB c Done: c $ node ./await-async.js AWAIT-LIB c/c/c Done: Error: ENOENT: no such file or directory, mkdir 'c/c/c' $ node ./await-async.js AWAIT-NATIVE d Done: d $ node ./await-async.js AWAIT-NATIVE d/d/d Done: Error: ENOENT: no such file or directory, mkdir 'd/d/d'
The program itself follows:
1 var nodefs = require('fs') 2 var async = require('asyncawait/async') 3 var await = require('asyncawait/await') 4 5 6 function ascertainDirCallback(path, callback) { 7 if ( 'string' === typeof path ) { 8 nodefs.mkdir(path, function(err) { 9 if (!err) callback(null, path) 10 else if ('EEXIST' === err.code) callback(null, path) 11 else callback(err, null) 12 }) 13 } else { 14 callback('mkdir: invalid path argument') 15 } 16 }; 17 18 19 function ascertainDirPromise(path) { 20 return new Promise(function(fullfill,reject) { 21 if ( 'string' === typeof path ) { 22 nodefs.mkdir(path, function(err) { 23 if (!err) fullfill(path) 24 else if ('EEXIST' === err.code) fullfill(path) 25 else reject(err) 26 }) 27 } else { 28 reject('mkdir: invalid path argument') 29 } 30 }); 31 } 32 33 34 function main() { 35 var method = 0 36 var dir = 0 37 var res = null 38 39 function usage() { 40 console.log('await-async.js CB/PROMISE/AWAIT-LIB/AWAIT-NATIVE directory') 41 process.exit(1) 42 } 43 44 switch ( process.argv[2] ) { 45 case 'CB': 46 case 'PROMISE': 47 case 'AWAIT-LIB': 48 case 'AWAIT-NATIVE': 49 method = process.argv[2] 50 break 51 default: 52 usage(); 53 } 54 55 dir = process.argv[3] 56 57 if ( process.argv[4] ) usage() 58 59 switch ( method ) { 60 case 'CB': 61 ascertainDirCallback(dir, function(err, path) { 62 console.log('Done: ' + (err ? err : path)) 63 }) 64 break 65 case 'PROMISE': 66 res = ascertainDirPromise(dir) 67 res.then(function(path) { 68 console.log('Done: ' + path) 69 },function(err) { 70 console.log('Done: ' + err) 71 }); 72 break 73 case 'AWAIT-LIB': 74 (async(function() { 75 try { 76 res = await(ascertainDirPromise(dir)) 77 console.log('Done: ' + res) 78 } catch(e) { 79 console.log('Done: ' + e) 80 } 81 })()); 82 break 83 case 'AWAIT-NATIVE': 84 (async function() { 85 try { 86 res = await ascertainDirPromise(dir) 87 console.log('Done: ' + res) 88 } catch(e) { 89 console.log('Done: ' + e) 90 } 91 })(); 92 break 93 } 94 } 95 96 main()
Please note:
- The anonymous function on line 74 would not be needed if main() itself was async()
- The anonymous function on line 84 would not be needed if main() itself was async
- A function that returns a Promise() (line 19) works as a async function without the async keyword.
Callback
Callback is the old simple method of dealing with asyncrounous things in JavaScript. A major complaint has been “callback hell”: if you call several functions in sequence it can get rather messy. I can agree with that, BUT I think each asyncrounous call deserves its own error handling anyway (and with proper error handling other options tend to be equally tedious).
Promise
I dont think using a promise (66-71) is very nice. It is of course a matter of habit. One thing is that not all requests in the success-path are actually success in real life, or not all errors are errors (like in ascertainDir). Very commonly you make a http-request which itself is good, but the data you receive is not good so you want to proceed with error handling. This means that the fulfill case needs to execute the same code as the reject case, for some “successful” replies. Promises can be chained, but it typically results in ignoring proper error handling.
awaitasync library
I think the syntax of the asyncawait library is rather horrible, but it works as a proof-of-concept for the real thing.
async await native keywords
With the async/await keywords in JavaScript, suddenly asyncrounous code can be handled just like in Java or C#. Since it is familiar it is appealing! No doubt it is clean and practical. I would hesitate to mix it with Callbacks or Promises, and would rather wait until I can do a complete rewrite.
Common sources of bugs in JavaScript are people trying to return from within (callback/promises) functions, people not realising the rest of the code continues to run after the asyncrous call, or things related to variable scopes. I guess in most cases the await/async makes these things cleaner and easier, but I would expect problems where it causes unexpected effects when not properly used.
Finally, if you start using async/await keywords there is no polyfill or fallback for older browser (maybe Babel can do that for you). As usual, IE seems to lag behind, and you can forget about Node v6 (or earlier). Depending on your situation, this could be a show stopper or no issue at all.
Watch something?
For more details, I can recommend this video on 5 architectures of asynchronous JavaScript.
0 Comments.