Nodejs is the king of this approach and thus is it's blessing (speed) and it's curse (see list below).
This approach forces a declarative manner of nested callbacks. There are quite a few problems with it:
- Mess. Callback nesting declarations add so much noise into program's logic that it becomes borderline-unreadable.
- Scoping. Normally, when you run loops, you expect each iteration's code share the same scope which is not the case with looping through async calls.
- Timing. Normally, when you run loops, you expect each iteration end before the next one starts. Again, not the case with looping through async calls.
- Exceptions. Normally, when you enclose code in try-catch clause, you expect all exceptions to be caught. Not the case with async.
Smart javascript developers attracted by elegance of idea behind Nodejs, but annoyed by the callback problems quickly realized that they can misuse generators to achieve more or less structured code without losing speed benefits which got them into this mess in the first place.
I am not going to cover generators here, but will provide my version of balance between elegance and speed to that end:
1. Install the following code by running npm install synchronode
npm install synchronode
Or download it from github: https://github.com/apodgorny/synchronode
module.exports = function(fGenerator) { var oG = fGenerator(), next = function(oError, oData) { // To distinguish between two different callback signatures -optional if (typeof oData == 'undefined') { oData = oError; oError = null; } if (oError) { return oG.throw(oError); } else { var oResult = oG.send(oData); if (!oResult.done) { oResult.value(next); } } } next(); } Function.prototype.sync = function() { var o = this, a = arguments; return function(fCallback) { a[a.length ++] = fCallback; o.apply(o, a); } }This code creates async loop between generator code in fGenerator and main function of the module, it's goal is to iterate through all yield statements within fGenerator function.
On each iteration it makes yield supply value which is normally sent by a callback of async call.
2. Use module:
// Require module var Sync = require('sync'); var Fs = require('fs'); // Sync is a controller that runs generator code Sync(function*() { // Call async function sync-ly, omitting last callback param // You do have to specify all optional params though // Value that you'd normally receive in callback, is yield'ed to your // regular program flow. Enjoy! var sContent = yield Fs.readFile.sync('myfile.txt', 'utf8'); console.log(sContent); });Loop example:
// Include sync module var Sync = require('sync'); // Declare your async routine as usual var authParther = function(sPartner, sToken, fCallback) { oDatabase.get('SELECT SOME AUTH RECORD', [sPartner, sToken], fCallback); } // Call async routine in a godly manner var authenticate = function(sToken, fCallback) { var bAuthenticated = false; Sync(function*() { var oUser; for (sPartner in ['facebook', 'google', 'linkedin']) { // sync method added to Function.prototype avoids having to // write a wrapper for every method on Earth // Nice and clean! if (oUser = yield authPartner.sync(sPartner, sToken)) { bAuthenticated = true; fCallback(oUser); } } } if (!bAuthenticated) { fCallback(null); } }