31

I have to interact with a remote api that forces me to chain requests. Thats a callback-hell in asynchronous mode:

// pseudocode: ajax(request_object, callback)
ajax(a, function() {
  ajax(b(a.somedata), function() {
    ajax(c(b.somedata), function() {
      c.finish()
    }
  }) 
})

It would be much more readable in sync mode:

sjax(a)
sjax(b(a.somedata))
sjax(c(b.somedata))
c.finish()

But Sjax is evil :) How do I do that in a nice not-so-evil and readable way?

defnull
  • 4,169
  • 3
  • 24
  • 23

9 Answers9

24

You could have a single function which is passed an integer to state what step the request is in, then use a switch statement to figure out what request needs to be make next:

function ajaxQueue(step) {
  switch(step) {
    case 0: $.ajax({
              type: "GET",
              url: "/some/service",
              complete: function() { ajaxQueue(1); } 
    }); break;
    case 1: $.ajax({
              type: "GET",
              url: "/some/service",
              complete: function() { ajaxQueue(2); }
            }); break;
    case 2: $.ajax({
              type: "GET",
              url: "/some/service",
              complete: function() { alert('Done!'); }
            }); break;
  }
}

ajaxQueue(0);

Hope that helps!

Jamie Rumbelow
  • 4,967
  • 2
  • 30
  • 42
  • You can't give arguments default values this way. But I like the solution. – Alsciende Jun 15 '09 at 11:53
  • Oh damn sorry, been programming a lot of PHP recently and my JavaScript's rusty. Posted updated with correct syntax :) – Jamie Rumbelow Jun 15 '09 at 12:03
  • ASP.NET 3.5's WebServices is set up like this. All the WebMethods on the JS side have a default callback function (well, if you set it up that way), and you can switch on the method name called on the server to decide what to do with the response. One thing I might change with this example is to not pass around numbers, but instead use more of an enum-like approach: ajaxQueue(some_name); so you know what your function call is doing. – Cᴏʀʏ Jun 15 '09 at 12:10
17

Don't use anonymous functions. Give them names. I don't know if you're able to do what I wrote below though:

var step_3 = function() {
    c.finish();
};

var step_2 = function(c, b) {
    ajax(c(b.somedata), step_3);
};

var step_1 = function(b, a) {
  ajax(b(a.somedata), step_2);
};

ajax(a, step_1);
Ionuț G. Stan
  • 176,118
  • 18
  • 189
  • 202
5

This function should chain together a list of ajax requests, if the callbacks always return the parameters necessary for the next request:

function chainajax(params, callbacks) {
  var cb = shift(callbacks);
  params.complete = function() {
    var newparams = cb(arguments);
    if (callbacks)
      chainajax(newparams, callbacks);
  };
  $.ajax(params);
};

You can define these callback functions separately and then chain them together:

function a(data) {
  ...
  return {type: "GET", url: "/step2.php?foo"}
};
// ...
function d(data) { alert("done!"); };

chainajax({type: "GET", url: "/step1.php"},
  [a, b, c, d]);

You could also declare the functions "inline" in the call to chainajax, but that might get a little confusing.

sth
  • 222,467
  • 53
  • 283
  • 367
4

Maybe what you can do is write a server-side wrapper function. That way your javascript only does a single asynchronous call to your own web server. Then your web server uses curl (or urllib, etc.) to interact with the remote API.

Apreche
  • 30,042
  • 8
  • 41
  • 52
3

Update: I've learn a better answer for this if you are using jQuery, see my update under the title: Using jQuery Deffered

Old answer:

You can also use Array.reduceRight (when it's available) to wrap the $.ajax calls and transform a list like: [resource1, resource2] into $.ajax({url:resource1,success: function(...) { $ajax({url: resource2... (a trick that I've learn from Haskell and it's fold/foldRight function).

Here is an example:

var withResources = function(resources, callback) {
    var responses = [];
    var chainedAjaxCalls = resources.reduceRight(function(previousValue, currentValue, index, array) {
        return function() {
            $.ajax({url: currentValue, success: function(data) {
                responses.push(data);
                previousValue();
            }})
        }
    }, function() { callback.apply(null, responses); });
    chainedAjaxCalls();
};

Then you can use:

withResources(['/api/resource1', '/api/resource2'], function(response1, response2) {
    // called only if the ajax call is successful with resource1 and resource2
});

Using jQuery Deffered

If you are using jQuery, you can take advantage of jQuery Deffered, by using the jQuery.when() function:

 jQuery.when($.get('/api/one'), $.get('/api/two'))
       .done(function(result1, result2) { 
              /* one and two is done */
        });
Diego
  • 4,353
  • 1
  • 24
  • 22
  • This is a nice, easy to follow JQuery explanation - https://medium.com/coding-design/writing-better-ajax-8ee4a7fb95f - note the section how you can also assign an $.ajax() call to a variable, and then call the .done() method on that, as a quick way to avoid indenting. – William Turrell Aug 12 '16 at 11:55
2

Check out this FAQ item on the jQuery site. Specially the callback reference and the complete method.

What you want is data from A to be passed to B and B's data passed to C. So you would do a callback on complete.

I haven't tried this though.

Ólafur Waage
  • 68,817
  • 22
  • 142
  • 198
2

I believe that implementing a state machine will make the code more readable:

var state = -1;
var error = false;

$.ajax({success: function() { 
                  state = 0;
                  stateMachine(); },
        error: function() {
                  error = true;
                  stateMachine();
        }});

function stateMachine() {
  if (error) {
     // Error handling
     return;
  }

  if (state == 0) {
    state = 1;
    // Call stateMachine again in an ajax callback
  }
  else if (state == 1) {

  }
}
kgiannakakis
  • 103,016
  • 27
  • 158
  • 194
  • If possible, one should avoid the use of global variables. Jamie Rumbelow's solution is a bit more elegant. – Alsciende Jun 15 '09 at 11:53
1

I made a method using Promises

// How to setup a chainable queue method
var sequence = Promise.resolve();

function chain(next){
    var promise = new Promise(function(resolve){
        sequence.then(function(){
            next(resolve);
        }); 
    });

    sequence = promise;
}

// How to use it
chain(function(next){
    document.write("<p>start getting config.json</p>");
    setTimeout(function(){
     document.write("<p>Done fetching config.json</p>");
        next();
    }, 3000);
});

chain(function(next){
    document.write("<p>start getting init.js</p>")
    setTimeout(function(){
        document.write("<p>starting eval scripting</p>");
        next();
    }, 3000);
});

chain(function(next){
    document.write("<p>Everything is done</p>");
});

Bonus: A ultraligth 138 byte limited A- Promise (that can only resolve - without parameters, and only call the last then-method )

Background: I made this for node.js at the point where it dose not have promises ATM. I didn't want a complete full blown Promise library that I was dependent on and had to include in my package.json, I needed it to be fast and light and do mostly one thing only. I only needed it for one thing (chaining things like you want to)

function Q(a,b){b=this;a(function(){b.then&&b.then();b.then=i});return b}function i(a){a&&a()}Q.prototype={then:function(a){this.then=a}};

How?

// Start with a resolved object
var promise = new Q(function(a){a()});
// equal to 
// var promise = Promise.resolve();

// example usage
new Q(function(resolve){
    // do some async stuff that takes time
    // setTimeout(resolve, 3000);
}).then(function(){
    // its done
    // can not return a new Promise
}); // <- can not add more then's (it only register the last one)

and for the chainable queue method

// How to setup a chainable queue method with ultraligth promise
var sequence = new Q(function(a){a()});

function chain(next){
    var promise = new Q(function(resolve){
        sequence.then(function(){
            next(resolve);
        }); 
    });

    sequence = promise;
}
Endless
  • 34,080
  • 13
  • 108
  • 131
0

The complete callback is what you're looking for:

$.ajax({
     type: 'post',
     url: "www.example.com",
     data: {/* Data to be sent to the server. It is converted to a query string, if not already a string. It's appended to the url for GET-requests. */},
     success:
          function(data) {
              /* you can also chain requests here. will be fired if initial request is successful but will be fired before completion. */
          },
    complete: 
         function() {
             /* For more a more synchronous approach use this callback. Will be fired when first function is completed. */
         }
});
DrewT
  • 4,983
  • 2
  • 40
  • 53