Finding and Fixing Ajax’s Elusive Bugs

Let’s start with some background. Google was an early pioneer of the Web development technique called “Ajax.” Ironically, when you Google the phrase “What is Ajax?” the search engine comes back with A Greek hero of the Trojan war, son of Telamon, king of Salamis. He was proverbial for his size and strength. 

AjaxIn Web development, however, AJAX is an acronym for Asynchronous JavaScript And XML. It’s a technique for refreshing only part of a Web page — rather than whole thing — from the server.

The AJAX technique has become an integral part of modern Web development, encapsulated in popular JavaScript libraries such as jQuery. In jQuery the X in Ajax isn’t just XML. It can also be text, HTML or JSON.

The Asynchronous nature of AJAX allows the browser to exchange data with a server behind the scenes, so to speak, while the user is doing other things. This is usually better than having them wait for an entire Web page to refresh.

Sample of AJAX jQuery Call

jQuery has several AJAX encapsulations including ajax, load, get and post.

Suppose, in an interactive Web application, you have the classic case of several one-to-many relationships in which a master record has several types of detail records related to it.

When the user changes the master record selection the  jQuery, AJAX call might look something like this:

<!-- The User Selects a New Master Record -->
<select id="master_record_selector" onchange="master_selection_has_changed(this.value);">
<option value="1">Master Record One</option>
<option value="2">Master Record Two</option>
<option value="3">Master Record Three</option>
</select>
//...
<script>
function master_selection_has_changed(new_key)
{
$.ajax({
type: 'POST',
url: 'set_master_record_session.php',
data: {id:new_key }
});
}
</script>
//...
/// Meanwhile ... back on the server ...
<? //set_master_record_session.php
// .... validation stuff ... //
$_SESSION['current_master_id'] = $_POST['id'];
$_SESSION['master_row'] = DB_QUERY("SELECT * FROM master_table WHERE id = ?",$_POST['id');
// Keep the current master information is session for performance reasons, fewer DB connects etc.
?>

With this ajax call, the function master_selection_has_changed sets the server session to reflect the current master record so subsequent  interaction can use the current record more quickly.

What's Wrong With This Code?

Suppose we want users to be able to edit the detail records associated with the master record using tab divs. We might make the code look like this:

<script>
function master_selection_has_changed(new_key)
{
$.ajax({
type: 'POST',
url: 'set_master_record_session.php',
data: {id:new_key }
});
$('#tab1').load('detail_form_1.php');
$('#tab2').load('detail_form_2.php');
}
</script>
//.... And on the server ...
<?
// detail_form_1.php
// Validate, etc ...
$master_id = $_SESSION['current_master_id'];
$master_record_name = $_SESSION['master_row']['name'];
$detail_record = database_call($master_id);
echo '<h4>Editing Detail 1 For :'.$master_record_name.'</h4>';
echo '<form id='detail_1_form'>';
echo '<input type="text" value="'.$detail_record['col1'].'" name="col1" id="col1" />';
//....
echo '<input type="hidden" name="master_id" value="'.$master_id.'" />';
echo '</form>';
?>

So what’s wrong with that? The code may very well work as expected most of the time. But occasionally a detail record will be posted to a previously selected master record. Why is that? Because the A in AJAX stands for Asynchronous.

With the code as presented, the form data in tab1 and tab2, unbeknownst to the hapless user, may not actually represent the detail data for the selected master record. Why not? Because the calls to load tabs 1 and 2 could actually be completed before the set session call.

Since the code will often appear to work as intended, finding this problem can be vexing.

So… What to Do?

The code above needs to be refactored to take into account the asynchronous nature of AJAX. When users change the master record we need to:

  1. Let users know that their forms are temporarily not available.
  2. When the detail forms are available, be sure that what they match the master record users selected.

Fortunately, this is a well understood problem and there are some standard solutions.

We’ve all see the little spinning “please wait” graphics on Web pages and other apps. So, as soon as the user changes the master record, we will “invalidate” the detail forms on the tabs with “please wait” spinners.

Also, we will not begin loading detail screens until after the session record information has been successfully updated. So, the new JavaScript code looks something like this:

<script>
function master_selection_has_changed(new_key)
{
// Invalidate the forms, so the user doesn't submit something while
// waiting for the master record to refresh.
$('#tab1').html('<img src="please_wait_spinner.gif" />');
$('#tab2').html('<img src="please_wait_spinner.gif" />');
$.ajax({
type: 'POST',
url: 'set_master_record_session.php',
data: {id:new_key },
success: function(data){
// Load the new tabs only after we know the master record session is good.
$('#tab1').load('detail_form_1.php');
$('#tab2').load('detail_form_2.php');
},
error: function(xhr, type, exception) {
alert("Your error message goes here");
}
});
}
</script>

This refactored code permits the necessary synchronization in an inherently asynchronous process.

Conclusion

The sample I presented here is a simplified presentation of a problem that vexed me for days. Some users never had any problems. Some users had problems all the time. Some testers could reproduce the problem. When I finally discovered the issue, fixed it and presented my findings to our CEO, I began by saying, “You know that AJAX thing that makes our app faster? The A in AJAX is for Asynchronous.”

I present this with the humble hope that with these lessons, you never have to have such a meeting with your CEO.

Comments

  1. BY joe turner says:

    Are there supposed to be empty div elements with id of ‘tab1′ and ‘tab2′ below the master record selector?

Post a Comment

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>