async parallel behavior with callback

The async library has a convenient method called parallel, which takes a collection of async functions to run and an optional callback to run once all the functions have completed successfully.

According to the documentation, it works as this:

Run the tasks collection of functions in parallel, without waiting until the previous function has completed. If any of the functions pass an error to its callback, the main callback is immediately called with the value of the error. Once the tasks have completed, the results are passed to the final callback as an array.

However, regarding whether the callback function gets invoked or not under some use cases, it can get very confusing and I got quite confused while reading a large legacy code base with callback hell. See below for the code and example output.

  • All tasks run but not callback
import async from 'async';

const task = {
  A: true,
  B: true,
};

async.parallel(
  [
    function (done) {
      if(task.A) {
        console.log('does A');
        return;
      }
      done(null);
    },
    function (done) {
      if(task.B) {
        console.log('does B');
        return;
      }
      done(null);
    },
  ],
  function (err) {
    if (err) {
      console.log(err);
      return;
    }
    console.log('does callback');
  },
);

/* console output
does A
does B
*/
  • One task runs but not callback
import async from 'async';

const task = {
  A: true,
  B: false,
};

async.parallel(
  [
    function (done) {
      if(task.A) {
        console.log('does A');
        return;
      }
      done(null);
    },
    function (done) {
      if(task.B) {
        console.log('does B');
        return;
      }
      done(null);
    },
  ],
  function (err) {
    if (err) {
      console.log(err);
      return;
    }
    console.log('does callback');
  },
);

/* console output
does A
*/
  • None task runs then callback runs
import async from 'async';

const task = {
  A: false,
  B: false,
};

async.parallel(
  [
    function (done) {
      if(task.A) {
        console.log('does A');
        return;
      }
      done(null);
    },
    function (done) {
      if(task.B) {
        console.log('does B');
        return;
      }
      done(null);
    },
  ],
  function (err) {
    if (err) {
      console.log(err);
      return;
    }
    console.log('does callback');
  },
);

/* console output
does callback
*/
  • One task runs with callback
import async from 'async';

const task = {
  A: true,
  B: false,
};

async.parallel(
  [
    function (done) {
      if(task.A) {
        setTimeout(() => {
          console.log('does A');
          done(null);
        }, 100);
        return;
      }
      done(null);
    },
    function (done) {
      if(task.B) {
        console.log('does B');
        return;
      }
      done(null);
    },
  ],
  function (err) {
    if (err) {
      console.log(err);
      return;
    }
    console.log('does callback');
  },
);

/* console output
does A
does callback
*/

As we can see, the callback only gets invoked when it eventually gets invoked normally from all the parallel functions. As for error cases, the doc is clear:

If any of the functions pass an error to its callback, the main callback is immediately called with the value of the error.

References

nodejs javascript