0

I am initializing my JS application by calling a number of methods asynchronously loading ressources, each of them depending of the previous ones. My intuitive solution is to nest the calls in callback functions (cba, cbb, cbc, cbd), beeing called from the inner of LoadA, LoadB, LoadC, LoadD respectively after these (LoadA, LoadB, LoadC, LoadD) were successfully completed:

app.LoadA( function cba() {
        app.LoadB( function cbb() {
            ....        
            app.LoadC( function cbc() {
            ...           
                app.LoadD( function cbd() {
                    ...
                } );
            } );
        } );
    } );

LoadA( cb )
{
    let url = '.........'; // just an url
    let req = new XMLHttpRequest();
    req.open('GET', url, true);
    req.responseType = 'arraybuffer';

    let lh = this;

    req.onload = function ol(event)         
    {
        let arrayBuffer = req.response; 
        let loadedObject = Convert( arrayBuffer, ......... );
        cb( loadedObject ); // call, if successed!
    }
    req.send(null);
}
....

LoadA returns without loading the object, so LoadB has to wait until LoadA's embedded onload function calls the callback cb and so on.

I don't like this nesting solution, as it is hard to overview and to maintain.

My question is: is there an other (focused on the "happy path", nicer, shorter, less confusing, easier to understand and maintain) possibility to achieve the same result?

Trantor
  • 768
  • 1
  • 8
  • 27
  • 1
    I _promise_ there's a better way of doing this: https://developers.google.com/web/fundamentals/getting-started/primers/promises – Andy Aug 30 '17 at 10:07
  • This is what is called a callback hell... Try searching for solutions to callback hell – Prasanna Aug 30 '17 at 10:10
  • Promises look to me like writing more confusing, lengthy and unmaintainable code... or is there a short way to use them with my example? – Trantor Aug 30 '17 at 10:11
  • You can use async await, it better to read – AkeRyuu Aug 30 '17 at 10:13
  • You should make your example more concrete, with actual values you want to return or accumulate, etc. Then the answer can be more to the point. – trincot Aug 30 '17 at 10:15
  • 3
    @Trantor the above code would be like `app.LoadA.then(app.LoadB).then(app.LoadC).then(app.LoadD).then(...)`. Is that really more confusing? – vassiliskrikonis Aug 30 '17 at 10:15
  • 1
    @Andy _then_ you better _await_ a reply from the OP. (sorry, that took me faaar too long) – evolutionxbox Aug 30 '17 at 10:37
  • 1
    @evolutionxbox Thanks for your _deferred_ response. – Andy Aug 30 '17 at 10:39
  • 1
    @Andy No problem. I hope it helped _resolve_ the issue. – evolutionxbox Aug 30 '17 at 10:40
  • 1
    @Andy, your exchange with evolutionxbox is most entertaining. Yes, I have enjoyed watching. I would definitely describe this conversation as _observable_. [cringes & puts down the shoe-horn] – Martin Joiner Aug 30 '17 at 11:03
  • 1
    @MartinJoiner I hope the conversation left you feeling _fulfilled_. – Andy Aug 30 '17 at 11:07

3 Answers3

2

Here is a comparison of "callback hell" and the nicer code you can achieve with async/await:

// dummy implementations: 
//  these functions call cb with value 1 to 4, after 100ms
const loadA = (cb) => setTimeout(_ => cb(1), 100);
const loadB = (cb) => setTimeout(_ => cb(2), 100);
const loadC = (cb) => setTimeout(_ => cb(3), 100);
const loadD = (cb) => setTimeout(_ => cb(4), 100);

function callbackHell() {
    loadA( function cba(a) {
        loadB( function cbb(b) {
            loadC( function cbc(c) {
                loadD( function cbd(d) {
                    console.log([a, b, c, d]);
                });
            });
        });
    });
}

async function nicerCode() {
    const res = [
        await new Promise(loadA),
        await new Promise(loadB),
        await new Promise(loadC),
        await new Promise(loadD)
    ];
    console.log(res);
}

callbackHell();
nicerCode();
.as-console-wrapper { max-height: 100% !important; top: 0; }
trincot
  • 317,000
  • 35
  • 244
  • 286
  • It's good for demonstration purposes, but IRL it's a bad idea to promisify callbacks just by passing them to Promise constructor because of error handling. – Estus Flask Aug 30 '17 at 11:11
  • @estus, of course, for error handling, the callback version of the function could accept an error callback function. The way functions can be promisified depends of course on their signature. With the given signature in the question, it could be done like this. Exceptions occurring in those functions should be captured with a `.catch` method. But mentioning all the ins and outs of using promises correctly is going well beyond the scope of the question in my opinion. – trincot Aug 30 '17 at 11:16
  • I'm thankful for any helpful suggestion :-) . But are you sure loadA will be called BEFORE loadB (and so on) in your solution ? – Trantor Aug 30 '17 at 11:25
  • 1
    Yes, that is what `await` does. It stops the function from execution further until the promised value becomes available. At that time the function's state is restored, and execution continues with the next statement. – trincot Aug 30 '17 at 11:47
2

To avoid callback hell, you'll need to use Promises.

If the loadA, ..., loadN functions return promises then you just call .then() after each one in order.

loadA().then(function() {
  loadB().then(function() {
    loadC().then(...
  });
});

What's now important is to remember that .then() return a Promise that resolves with the value of it's argument.

So if both loadA and loadB return Promises, you can just chain them like so:

loadA().then(function() {
  return loadB();
).then(...)

which is equivalent to this:

loadA().then(loadB).then(loadC).then(...)

Much simpler!

If your function doesn't return a Promise, then you need to wrap them inside one with a helper function.

function wrapInsidePromise(f) {
  return new Promise(function(resolve, reject) {
    f(function() {
      resolve();
    });
  });
}

var pLoadA = wrapInsidePromise(app.loadA);
var pLoadB = wrapInsidePromise(app.loadB);
...

pLoadA().then(pLoadB).then(...);

What's more, in ES6 you can use async/await which let's you use Promises in an asynchronous way.

async function() {
  await loadA();
  await loadB();
  ...
  let finalResult = await loadN();
  ...
}
vassiliskrikonis
  • 566
  • 2
  • 10
  • Yes, the final async function looks really nice, but I need the load calls in a special sequence ... (a,b,c,d...) – Trantor Aug 30 '17 at 11:39
  • 1
    @Trantor what do you mean? With `await` those asynchronous calls will get called _in this exact sequence_, one after the other. – vassiliskrikonis Aug 30 '17 at 12:57
  • 1
    You seem to have forgotten to invoke most of the promise-returning functions in your snippets. – Bergi Aug 31 '17 at 09:34
  • 1
    Also, that's *not* how the [`Promise` constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) works [for promisification](https://stackoverflow.com/q/22519784/1048572) – Bergi Aug 31 '17 at 09:35
  • 1
    @Bergi oops! Thanks!! I've edited my answer, I suppose it's now correct...? – vassiliskrikonis Aug 31 '17 at 10:34
1

I would break this up using a "Promise" for each async call.

MDN have a great explanation of how to use them here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises

Your app code could end up looking something like this...

app = {

    loadA: function() {
        return new Promise((resolve) => {
            console.log('Loading A');
            resolve();
        });
    },

    loadB: function () {
        return new Promise((resolve) => {
            console.log('Loading B');
            resolve();
        });
    },

    // For demonstration, I have coded loadC with a rejection if it fails
    // (Success is based on a random boolean) 
    loadC: function () {
        return new Promise((resolve, reject) => {
            console.log('Loading C');
            var success = Math.random() >= 0.5;
            if( success ){
                resolve();
            } else {
                reject('C did not load');
            }

        });
    },

    loadD: function () {
        return new Promise((resolve) => {
            console.log('Loading D');
            resolve();
        });
    }

};

// A global function for reporting errors
function logError(error) {
    // Record the error somehow
    console.log(error);
}

Now call them in a promise chain

app.loadA().then(app.loadB).then(app.loadC, logError).then(app.loadD);

Now, if in the future you decide to change the order you want your functions called, you don't need to touch the code in the functions at all because you used promises. You can just change the line containing the promise chain:

app.loadA().then(app.loadD).then(app.loadB).then(app.loadC, logError);

(The above example assumes you can change the methods on app to work as promises. If app is a 3rd party thing that you cannot edit the solution would be different)

Martin Joiner
  • 3,529
  • 2
  • 23
  • 49
  • Promises look to me like writing more confusing, lengthy and unmaintainable code... or is there a short way to use them with my example? – Trantor Aug 30 '17 at 10:15
  • Hold on. Do you want to write short code, maintainable code or ECMA 6? Your question suggests you want to jump in with all the shiny new ECMA 6 stuff, but your comments are rejecting everyone suggesting promises (a very ECMA 6 thing). And now you want maintainable code but also short code? Often, to make code more maintainable you write it in a longer way (modulated). – Martin Joiner Aug 30 '17 at 10:29
  • 1
    While I totally agree on using promises, this is not what it should look like. This is still nesting everything (but using the flattening-by-function-declaration technique), a promise chain should look like `loadA().then(loadB).then(loadC).then(loadD)`. – Bergi Aug 31 '17 at 09:22
  • @Bergi Feel free to edit my answer to make my snippet better. I personally would like to improve my understanding of Promises so I'd appreciate it. – Martin Joiner Aug 31 '17 at 09:38
  • Thanks for the edit, that looks much better now. You might even want to provide the complete bodies of the promise creations, calling into the callback-based `app` methods – Bergi Aug 31 '17 at 09:42
  • @Bergi Yes I would like to do that. I am just hacking out a solution in a test file before I edit my answer again :-) – Martin Joiner Aug 31 '17 at 09:46
  • @Bergi What do you think now? – Martin Joiner Aug 31 '17 at 10:16
  • Just edited my question to clarify the behaviour of LoadA (B,C,D) : LoadA returns without loading the object, so LoadB has to wait until LoadA's embedded onload function calls the callback cb and so on. Do you think your solution would still fit? – Trantor Aug 31 '17 at 12:21