Recent Posts

Showing posts with label AJAX. Show all posts
Showing posts with label AJAX. Show all posts

Javascript: How to return response from asynchronous ajax call

The next piece of code tries to return the response fron an ajax asynchronous call but with this we can only get undefined values:


function foo() {
    var result;

    $.ajax({
        url: '...',
        success: function(response) {
            result = response;
            // return response; // <- tried that one as well
        }
    });

    return result;
}

var result = foo(); // always ends up being `undefined`.

The explanation of this ajax asynchronous methods


The A in AJAX stands for asynchronous. That means sending the request (or rather receiving the response) is taken out of the normal execution flow. In your example, $.ajax returns immediately and the next statement, return result;, is executed before the function you passed as success callback was even called.

Here is an analogy which hopefully makes the difference between synchronous and asynchronous flow clearer:

Synchronous


Imagine you make a phone call to a friend and ask him to look something up for you. Although it might take a while, you wait on the phone and stare into space, until your friend gives you the answer you needed.

The same is happening when you make a function call containing "normal" code:

function findItem() {
    var item;
    while(item_not_found) {
        // search
    }
    return item;
}

var item = findItem();
// do something with item
doSomethingElse();
Even though findItem might take a long time to execute, any code coming after var item = findItem(); has to wait until the function returns the result.

Asynchronous

You call your friend again for the same reason. But this time you tell him that you are in a hurry and he should call you back on your mobile phone. You hang up, leave the house and do whatever you planned to do. Once your friend calls you back, you are dealing with the information he gave to you.

That's exactly what's happening when you do an AJAX request.

findItem(function(item) {
    // do something with item
});
doSomethingElse();
Instead of waiting for the response, the execution continues immediately and the statement after the AJAX call is executed. To get the response eventually, you provide a function to be called once the response was received, a callback (notice something? call back ?). Any statement coming after that call is executed before the callback is called.

Solution(s)


Embrace the asynchronous nature of JavaScript! While certain asynchronous operations provide synchronous counterparts (so does "Ajax"), it's generally discouraged to use them, especially in a browser context.

Why is it bad do you ask?


JavaScript runs in the UI thread of the browser and any long running process will lock the UI, making it unresponsive. Additionally, there is an upper limit on the execution time for JavaScript and the browser will ask the user whether to continue the execution or not. All of this is really bad user experience. The user won't be able to tell whether everything is working fine or not. Furthermore the effect will be worse for users with a slow connection.

Restructure code


Let functions accept callbacks

The better approach is to organize your code properly around callbacks. In the example in the question, you can make foo accept a callback and use it as success callback. So this

var result = foo();
// code that depends on 'result'
becomes:

foo(function(result) {
    // code that depends on 'result'
});
Here we pass a function as argument to foo. You can pass any function reference, for example:

function myCallback(result) {
    // code that depends on 'result'
}

foo(myCallback);
foo itself is defined as follows:

function foo(callback) {
    $.ajax({
        // ...
        success: callback
    });
}
callback will refer to the function we pass to foo when we call it and we simply pass it on to success. I.e. once the AJAX request is successful, $.ajax will call callback and pass the response to the callback (which can be referred to with result, since this is how we defined the callback).

You can also process the response before passing it to the callback:

function foo(callback) {
    $.ajax({
        // ...
        success: function(response) {
            // e.g. filter the response
            callback(filtered_response);
        }
    });
}
It's easier to write code using callbacks than it seems. After all, JavaScript in the browser is heavily event driven (DOM events). Receiving the AJAX response is nothing else but an event.
Difficulties could arise when you have to work with third party code, but most problems can be solved by just thinking through the application flow.

Use promises


The Promise API is a new feature of ECMAScript 6, but it has good browser support already. There are also many libraries which implement the standard Promises API and provide additional methods to ease the use and composition of asynchronous functions (e.g. bluebird).

Promises are containers for future values. When the promise receives the value (it is resolved) or when it is cancelled (rejected), it notifies all of its "listeners" who want to access this value.

The advantage over plain callbacks is that they allow you do decouple your code and they are easier to compose.

Here is a simple example of using a promise:

function delay() {
  // `delay` returns a promise
  return new Promise(function(resolve, reject) {
    // only `delay` is able to resolve or reject the promise
    setTimeout(function() {
      resolve(42); // after 3s, resolve the promise with value 42
    }, 3000);
  });
}

delay().then(function(v) { // `delay` returns a promise
  console.log(v); // log the value once it is resolved
}).catch(function(v) {
  // or do something else if it is rejected 
  // (would not happen in this example, since `reject` is not called
});
Applied to our Ajax call we could use promises like this:

function ajax(url) {
  return new Promise(function(resolve, reject) {
    var xhr = new XMLHttpRequest();
    xhr.onload = function() {
      resolve(this.responseText);
    };
    xhr.onerror = reject;
    xhr.open('GET', url);
    xhr.send();
  });
}

ajax("/echo/json").then(function(result) {
  // code depending on result
}).catch(function() {
  // an error occurred
});
Describing all the advantages that promises offer is beyond the scope of this answer, but if you write new code, you should seriously consider them. They provide a great abstraction and separation of your code.

More information about promises: HTML5 rocks - JavaScript Promises

jQuery: Use deferred objects


Deferred objects are jQuery's custom implementation of promises (before the Promise API was standardized). They behave almost like promises, but expose a slightly different API.

Every AJAX method of jQuery already returns a "deferred object" (actually a promise of a deferred object) which you can just return from your function:

function ajax() {
    return $.ajax(...);
}

ajax().done(function(result) {
    // code depending on result
}).fail(function() {
    // an error occurred
});
Promise gotchas

Keep in mind that promises and deferred objects are just containers for a future value, they are not the value itself. For example suppose you had the following:

function checkPassword() {
    return $.ajax({
        url: '/password',
        data: {
            username: $('#username').val()
            password: $('#password').val()
        },
        type: 'POST',
        dataType: 'json'
    });
}

if (checkPassword()) {
    // Tell the user they're logged in
}
This code misunderstands the above asynchrony issues. Specifically, $.ajax() doesn't freeze the code while it checks the '/password' page on your server - it sends a request to the server and while it waits, immediately returns a jQuery Ajax Deferred object, not the response from the server. That means the if statement is going to always get this Deferred object, treat it as true, and proceed as though the user is logged in. Not good.

But the fix is easy:

checkPassword()
.done(function(r) {
    if (r) {
        // Tell the user they're logged in
    } else {
        // Tell the user their password was bad
    }
})
.fail(function(x) {
    // Tell the user something bad happened
});
So now we're still calling the '/password' page on the server, but our code now properly handles the wait time for the server to respond. The $.ajax() call still returns immediately with a jQuery Ajax Deferred object, but we use it to attach event listeners to .done() and .fail(). In the .done() call, where the server responded with a normal response (HTTP 200), we check the object returned by the server. In this example the server is just returning true if the login was successful, false if not, so if (r) is checking for true/false.

In the .fail() handler we're dealing with something going wrong - for example if the user lost their internet connection while they were typing in their username and password, or if your server went down.

Not recommended: Synchronous "AJAX" calls

As I mentioned, some asynchronous operations have synchronous counterparts. While I don't advocate there use, for completeness, here is how you would perform a synchronous call:

Without jQuery

If you directly use a XMLHTTPRequest object, pass false as third argument to .open.

jQuery

If you use jQuery, you can set the async option to false. Note that this option is deprecated since jQuery 1.8. You can then either still use a success callback or access the responseText property of the jqXHR object:

function foo() {
    var jqXHR = $.ajax({
        //...
        async: false
    });
    return jqXHR.responseText;
}
If you use any other jQuery AJAX method, such as $.get, $.getJSON, etc., you have to change it to $.ajax (since you can only pass configuration parameters to $.ajax).

Heads up! It is not possible to make a synchronous JSONP request. JSONP by its very nature is always asynchronous (one more reason to not even consider this option).

How to: Perform an AJAX Synchronous request

From the Jquery docs: you specify the async option to be false to get a synchronous Ajax request. Then your callback can set some data before your mother function proceeds.

Here's what your code would look like if changed as suggested:

beforecreate: function(node,targetNode,type,to) {
    jQuery.ajax({
         url:    'http://example.com/catalog/create/' 
                  + targetNode.id 
                  + '?name=' 
                  + encode(to.inp[0].value),
         success: function(result) {
                      if(result.isOk == false)
                          alert(result.message);
                  },
         async:   false
    });          
}

Aborting AJAX request with jQuery

Most of the jQuery Ajax methods return an XMLHttpRequest (or the equivalent) object, so you can just use abort().

See the documentation:

abort Method (MSDN). Cancels the current HTTP request.
abort() (MDN). If the request has been sent already, this method will abort the request.

var xhr = $.ajax({
    type: "POST",
    url: "some.php",
    data: "name=John&location=Boston",
    success: function(msg){
       alert( "Data Saved: " + msg );
    }
});

//kill the request
xhr.abort()
As of jQuery 1.5 the returned object is a wrapper for the native XMLHttpRequest object called jqXHR. This object appears to expose all of the native properties and methods so the above example still works. See The jqXHR Object (jQuery API documentation).

Example: jQuery AJAX POST with PHP

Basic usage of .ajax would look something like this:

HTML:

<form id="foo">

    <label for="bar">A bar</label>
    <input id="bar" name="bar" type="text" value="" />

    <input type="submit" value="Send" />

</form>
JavaScript:

// variable to hold request
var request;
// bind to the submit event of our form
$("#foo").submit(function(event){
    // abort any pending request
    if (request) {
        request.abort();
    }
    // setup some local variables
    var $form = $(this);
    // let's select and cache all the fields
    var $inputs = $form.find("input, select, button, textarea");
    // serialize the data in the form
    var serializedData = $form.serialize();

    // let's disable the inputs for the duration of the ajax request
    // Note: we disable elements AFTER the form data has been serialized.
    // Disabled form elements will not be serialized.
    $inputs.prop("disabled", true);

    // fire off the request to /form.php
    request = $.ajax({
        url: "/form.php",
        type: "post",
        data: serializedData
    });

    // callback handler that will be called on success
    request.done(function (response, textStatus, jqXHR){
        // log a message to the console
        console.log("Hooray, it worked!");
    });

    // callback handler that will be called on failure
    request.fail(function (jqXHR, textStatus, errorThrown){
        // log the error to the console
        console.error(
            "The following error occured: "+
            textStatus, errorThrown
        );
    });

    // callback handler that will be called regardless
    // if the request failed or succeeded
    request.always(function () {
        // reenable the inputs
        $inputs.prop("disabled", false);
    });

    // prevent default posting of form
    event.preventDefault();
});
Note: Since jQuery 1.8, .success, .error and .complete are deprecated in favor of .done, .fail and .always.

Note: Remember that the above snippet has to be done after DOM ready, so you should put it inside a $(document).ready() handler (or use the $() shorthand).

Tip: You can chain the callback handlers like this: $.ajax().done().fail().always();

PHP (i.e. form.php):

// you can access the values posted by jQuery.ajax
// through the global variable $_POST, like this:
$bar = $_POST['bar'];
Note: Always sanitize posted data, to prevent injections and other malicious code.

You could also use the shorthand .post in place of .ajax in the above JavaScript code:

$.post('/form.php', serializedData, function(response) {
    // log the response to the console
    console.log("Response: "+response);
});
Note: The above JavaScript is made to work with jQuery 1.8 but should work with previous versions down to jQuery 1.5.