The problem in this part:
funcArray.forEach(function(f) { f(x) i++; if(i == 2) { doneAll(result.arr) } });
which is a synchronous function, so when you check if(i == 2) , you basically check that you have called all async functions, but they haven't returned anything yet, so all you know is that the functions were called, but result.arr is not populated yet.
You must move the doneAll(result.arr) expression doneAll(result.arr) to the child callback, then it will be called by the async function because it returns the result.
A simple solution that I can think of is writing your child as
function child(arg) { if (this.arr.push(arg) === this.allCount) this.doneAll(this.arr); }
and in your waiter function waiter improve the result object
var result = { arr: [] , allCount: funcArray.length , doneAll: doneAll };
This will work, but it has one drawback - the position of the results does not save the position of the functions in the funcArray , the position of the results is sorted by the duration of the asynchronous function, just the first one will accept the first result, etc. If this is a problem, you should also pass the index of your child function to store the result at a valuable position in the result array, and then checking arr.length will not work, because the JS array returns the length as the highest index + 1, so if your last funcArray will execute first, it will fill in the last index, and the length of result.arr will be this.allCount , so to keep the order of the result equal to funcArray , you will need to save the number of returned results as a different number, increase this number with each new result and compare this number with allCount .
Or reduce allCount this way
function child(idx, arg) { this.arr[idx] = arg; if (--this.allCount === 0) this.doneAll(this.arr); }
And change your waiter function
function waiter(funcArray, doneAll) { const result = { arr: [] , allCount: funcArray.length , doneAll: doneAll }; funcArray.forEach(function(f, i) { f(child.bind(result, i)); }); }