max-mapper/callback-hell

Mention async.js

darobin opened this issue · 6 comments

Good stuff! I think it's worth also mentioning async.js (https://github.com/caolan/async/). I use it a lot to keep async stuff in check, especially parallel().

first and foremost I want to cover simple and 'pure js' solutions to common problems without resorting to third party libs.

however I also use async in certain cases and see the value in libraries that let you run a callback on a group of callbacks

jquery deferreds do it like this (syntax from memory):

$.when(promise, promise, promise).then(callback)

async does it like this:

async.parallel([function, function, function], callback)

minimal libs like queue do it like this:

queue(1).defer(function).defer(function).defer(function).await(callback)

at a high level these are the same pattern. async has lots of options and flavors, queue is very minimal at 554 bytes, jquery deferreds are syntax heavy and complex but also pervasive

maybe i could distill these thoughts into a description of the 'callback for many callbacks' problem somehow?

heres a simplified version of the question:

say you want to download two cat photos and combine them into one cat photo:

downloadKitten('http://placekitten.com/200/600', firstKitten)
downloadKitten('http://placekitten.com/200/300', secondKitten)

whats the simplest way to get both kittens in one callback?

naive implementation:

function waitForTwoCats(catURL1, catURL2, callback) {
  var first, second
  function firstKitten(firstCat) {
    first = firstCat
    if (second) callback(first, second)
  }
  function secondKitten(secondCat) {
    second = secondCat
    if (first) callback(first, second)
  }
  downloadKitten(catURL1, firstKitten)
  downloadKitten(catURL2, secondKitten)
}

obviously there are problems with this but the goal is to show when you need third party libs and when you dont. is this the simplest way to show the complexity of the problem?

For me, the other case of callback hell, besides "wait till this set of async things finish" and nested callbacks is code that has conditionals. Suppose that there are some steps, some of which need to happen async, but some that can be skipped if a value is available already.

function step3(value) {
   //Do step 3
}

function step2(value) {
  //Do step 2
   step3(value);
}

function step1() {
    //Do step 1
    step2(value);
}

if (alreadyHaveValue()) {
       step2(existingValue);
   } else {
      step1();
   }
}

For me, that is when something like promises or some library with a .then() which allows processing plain values or deferred promises, comes in useful. Using q:

Q.fcall(function () {
    if (alreadyHaveValue()) {
        //Existing value is not a promise
        return existingValue;
    } else {
        return step1ReturningPromiseOrJustInlineHere();
    }
}).then(function (value) {
    //Do step 2, could have conditionals
}).then(function (value) {
   //Do step 3, could have conditionals
}); 

It gives a more linear read of the logic flow.

No need for inside out, functions with names that are jumped to for the next step. That is too reminiscent of goto.

mixu commented

I hate plugging my own solutions, but hey, you asked on Twitter. Maybe you can reduce this problem into three basic functions, series(), parallel() and limited()? http://book.mixu.net/ch7.html

Would love a link back if you do use it, though these days I'd write a lot of that stuff differently, works though.

I think async.js certainly worth some words.
https://www.npmjs.com/package/async: 9,178,397 downloads in the last month
https://www.npmjs.com/package/q: 3,664,443 downloads in the last month
https://www.npmjs.com/package/promise: 745,687 downloads in the last month

And the link to @mixu's book is now http://book.mixu.net/node/ch7.html.