Unexpected Error from GetListItemsChangesSinceToken Using SPServices

An unexpected result from GetListItemChangesSinceToken using SPServicesThis is another post in the vein of “it happened to me and it took me a long time to figure it out, so why should you go through the same pain”?

I’ve got an application running at a client that sits on top of SharePoint 2007. It uses KnockoutJS and lots of other modern Web development goodies, even though we’re on a very old SharePoint version. In fact, most of the time, I’m debugging in Internet Explorer 8, because that’s still the corporate standard.

Fun stuff, right? Well, the application serves them great, and I still get to have fun because it’s modern development.

Over the last few weeks, I’ve been trying to figure out why some of my calls to the SPServices function SPGetListItemsJson haven’t been returning what I expected. The calls were happening and I was getting valid responses in Firefox and Chrome, but in IE8, the data wasn’t all there. I was using localStorage to store a local cache of the data so I had data from weeks back there, but more recent data wasn’t coming back from my calls. (See Caching SharePoint Data Locally with SPServices and HTML5’s Web Storage for information on storing data locally in the browser cache.)

You can say what you want about my skills, but debugging Web Services calls in IE8 is well-nigh impossible. There’s simply no straightforward way to see the packets going out and back. Adding to the difficulty, Fiddler won’t run properly in this environment and the machine I remote into is locked down so that I can’t update it. Yup, three hands tied behind my back.

I finally got into the bowels of one of the calls using the IE8 Developer Tools (ick) and grabbed the innerHtml from the call. Here’s what I saw:

<xml:namespace prefix = soap />
<xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <?XML:NAMESPACE PREFIX = [default] http://schemas.microsoft.com/sharepoint/soap/ NS = "http://schemas.microsoft.com/sharepoint/soap/" />
    <GetListItemChangesSinceTokenResponse xmlns="http://schemas.microsoft.com/sharepoint/soap/">
      <GetListItemChangesSinceTokenResult>
        <listitems xmlns:rs="urn:schemas-microsoft-com:rowset">
          <Changes>
            <Id ChangeType="InvalidToken">
            </Id>
          </Changes>
          <?xml:namespace prefix = rs />
          <rs:data ItemCount="0">
          </rs:data>
        </listitems>
      </GetListItemChangesSinceTokenResult>
    </GetListItemChangesSinceTokenResponse>
  </soap:Body>
</soap:Envelope>

Note the highlighted lines. I’d never run into that response before, so off the to MSDN documentation for GetListItemsChangesSinceToken, which underlies the SPGetListItemsJson function.

The basic documentation page for GetListItemsChangesSinceToken doesn’t say anything about InvalidToken and I’d never run into it before, so I had no idea what it meant. But in an article marked as

This content is outdated and is no longer being maintained. It is provided as a courtesy for individuals who are still using these technologies. This page may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist.

(Hey, we’re working with SharePoint 2007 here) there’s an explanation of several other possible responses to calls to the function:

Table 4: Change Events

Change Events Description
<Id ChangeType=”InvalidToken” /> The token is either invalid or old. You must do a full synchronization.
<Id ChangeType=”Restore” /> The list has been restored from the recycle bin or from a backup. You should perform a full synchronization.
<List></List> If the list schema has changed, or if a change token was not provided, the complete list is returned. The format is the same as returned by GetList.

In the first two cases above, the client should ignore other changes and do a full reconciliation of the list.

(See: https://msdn.microsoft.com/en-us/library/cc264031(v=office.12).aspxThe )

Like I said, I’ve never seen this response, so I had never built anything into SPGetListItemsJson to handle it.

The issue really came down to the fact that I was using localStorage rather than sessionStorage and I’d not built in a capability to occasionally throw away the localStorage cache and fully refresh it. Yes, my bad.

The quick fix was to switch to storing the cache in sessionStorage instead. On a lot of levels, that’s a good idea, anyway. Using localStorage means that I have a persistent cache, but if something goes wrong with that cache – corruption, or something exactly like this – there’s no quick fix. By switching to sessionStorage, I’ve made the cache volatile in that it will be purged at the end of each browser session. Now when someone has a problem with their cache (something they won’t ever understand from a user perspective), I can simply say “Please shut down your browser and restart. Let me know if that fixes the problem.”

I hope this helps someone else along the way.

Getting Around SharePoint’s Threshold Limits for Large Reference Lists

In SharePoint 2007, we could build lists that contained as many items as we wanted and access them in any way we wanted. We may have done stupid things that were bad for server performance, but if we knew what we were doing there were few limits. We had the mythical “2000 item” rule of thumb (which had little basis in reality in many, many cases) but otherwise it was up to us. (See: Plan for software boundaries (Office SharePoint Server))

In SharePoint 2010, Microsoft introduced threshold limits to protect us from ourselves. This seemed counter intuitive to me, since with the upgrade to SharePoint 2010, one also had to go to 64 bit architecture and put a heck of a lot more iron behind the SharePoint farm. While we could potentially store 30,000,000 items in a list (30 million!), we had a list view threshold of 5000 items. (See: SharePoint Server 2010 capacity management: Software boundaries and limits)

SharePoint 2013 maintains similar limits to 2010, with 5000 items the limit for a list view. (See: Software boundaries and limits for SharePoint 2013)

List View ThrottlingSomehow, as technology has moved forward – significantly – we’ve got more limits.

5000 items is way too many to ever show in a list view, but it may not be if you want to do some client side processing. It doesn’t matter if you’re trying to build a truly bloated, Bad Idea list view or request data using REST, you’re stuck with that 5000 item limit.

But what if you know what you are doing and the 5000 item limit doesn’t work for you? I’ve written in the past about why we shouldn’t change some of the threshold limits (e.g.,  The SharePoint 2010 “List View Lookup Threshold” and Why We Don’t Change It).

If we’re working on Office365, we simply can’t change the limits – Microsoft gets to decide what they are. Sometimes those limits change without notice, too, so it’s truly out of our hands.

Well, what does all this add up to? Let’s take a specific example.

I needed to provide auto complete functionality in an InfoPath form. It was the classic scenario: we had a list of [in this case] 19,000+ cities and towns and we needed a good UX for entering selections into the form. We run into this sort of thing with data like cities and towns all the time. There are too many choices for a dropdown to make any sense, and SharePoint doesn’t offer up any better alternatives. jQueryUI’s autocomplete function is a perfect solution for this sort of thing.

The data was in a spreadsheet, and it’s easy to upload data from a spreadsheet to a SharePoint list. Bingo, bango, done, right? Well, no. It’s impossible to upload data from a spreadsheet *and* index the column. We need to index the column so that we can make type of requests requests that we need for autocomplete – basically, give me all of the items that begin with or contain this string – or else we run into the 5000 item threshold error on our requests.

No problem! We’ll just upload the data and *then* index the column. Not so fast, mister. When you try to index the column you run into – you guessed it – the 5000 item limit. No indexee, no laundry.

So seemingly we’re stuck. But wait – we can just create the list in the UI, add the index, and paste all 19,000 items into the Brave New World of SharePoint 2013’s “data sheet view” aka “Quick Edit”. Sadly, when I tried to paste in more than a few hundred items, the browser hung on me. This was on Office365, and I tried several different browsers. SharePoint Online seems to prefer nibbles to big bites of data. Unless I spent days just doing the pasting, this was never going to happen.

In this case, I decided to simply store the data in a text file containing JSON rather than trying to force it into a list. Thanks to @bpmccullough for the suggestion on Twitter.

The data I originally had received for the sities and towns was in a CSV file. I found a nice little site that would convert the CSV format to JSON (bookmark this one; you’ll need it in this JSON-oriented world).

Once I had the JSON file uploaded to a Document Library, the code below is what I used to set up the autocomplete. We wanted the autocomplete to allow multiple choices, so there’s a little monkeying around to enable that. It wasn’t tremendously peppy (19,000 cities, after all), but it worked. Remember, kids: this is an example. Don’t expect to drop it into your page and have it work.

var citiesAndStates = [];

// Get the City and State data from the file with JSON data
$.getJSON(&quot;/Shared%20Documents/CityState.txt&quot;, function(data) {
  var results = $(data);
  // Push the data into an array which is appropriate for jQueryUI autocomplete
  citiesAndStates = ($.map(results, function(item) {
    return {
      label: item.State + &quot; - &quot; + item.City, // Show &quot;State - City&quot; [e.g., MA - Boston] for selection
      value: item.City + &quot;, &quot; + item.State // Show &quot;City, State&quot; [e.g., Boston, MA] upon selection
    }
  }));
});

// When the field is available and gets focus, set up the autocomplete behavior
//ctl00_ctl42_g_6069846d_1b7f_4890_b767_2bdc15d2b133_FormControl0_V1_I1_S15_I4_T5
$(document).on(&quot;focus&quot;, &quot;input[id$='FormControl0_V1_I1_S15_I4_T5']&quot;, function() {
  if ($(this).data(&quot;autocompleteSet&quot;) === undefined) {
    $(this).data(&quot;autocompleteSet&quot;, true);

    $(this)
    // Don't navigate away from the field on tab when selecting an item
    .bind(&quot;keydown&quot;, function(event) {
      if (event.keyCode === $.ui.keyCode.TAB &amp;&amp; $(this).data(&quot;ui-autocomplete&quot;).menu.active) {
        event.preventDefault();
      }
    }).autocomplete({
      source: function(request, response) {
        // Delegate back to autocomplete, but extract the last term
        response($.ui.autocomplete.filter(citiesAndStates, extractLast(request.term)));
      },
      minLength: 3,
      focus: function() {
        // Prevent value inserted on focus
        return false;
      },
      select: function(event, ui) {
        var terms = split(this.value);
        // Remove the current input
        terms.pop();
        // Add the selected item
        terms.push(ui.item.value);
        // Add placeholder to get the semicolon-and-space at the end
        terms.push(&quot;&quot;);
        this.value = terms.join(&quot;; &quot;);
        return false;
      }
    });
  }
});

function split(val) {
  return val.split(/;\s*/);
}

function extractLast(term) {
  return split(term).pop();
}

One note here: This approach basically violates my “put all data into a list so that end users can maintain it” rule. However, the approach I came up with was the best one for the situation. It’s still possible for a reasonably savvy (meaning “non-developer”) user to manage the data by downloading the file and using a JSON-aware editor.

The main thing was that we were able to set up something that worked, regardless what the Office365 limits may change to.

Finding the SharePoint 2007 / 2010 Thesaurus Files

If you go to TechNet to find out where the thesaurus files for search are so that you can add in your own synonyms, you may be as confused as I was earlier today. It only took me about 20 minutes to figure out, but if three or four people find this post, we’ll have saved enough time for lunch.

The TechNet article you want is Manage thesaurus files (SharePoint Server 2010), though the one for SharePoint 2007 (Edit a thesaurus file (Office SharePoint Server)) is pretty much identical.

In the article, it says

By default, SharePoint Server 2010 installs the thesaurus files for all supported languages at %ProgramFiles%\Microsoft Office Servers\14.0\Data\Office Server\Config. When a search administrator creates a Search service application, the search system automatically copies the thesaurus files from the installation location (including any thesaurus files there that an administrator has edited) to %ProgramFiles%\Microsoft Office Servers\14.0\Data\Office Server\Applications\GUID-query-0\Config, where GUID is the GUID of the new Search service application. The search system performs the same operation on every query server that is running the new Search service application. Thus there is a copy of each thesaurus file on each query server that is running that Search service application.

When I looked in %ProgramFiles%\Microsoft Office Servers\14.0\Data\Office Server\Config, well, there was no %ProgramFiles%\Microsoft Office Servers\14.0\Data\Office Server\Config. Instead, because my client had decided to change the location of the index to another drive, I have to figure out where that actually was. Here’s the trick.

In complex farms, you may have multiple Search Service Application, multiple indices, etc. but these steps should work in most cases.

  • In Central Administration, go to the Search Application -> Central Administration/ Manage service applications / Search Service Application (or whatever you called it)
  • At the bottom of the page, you’ll see a section called ‘Search Application Topology’
  • Click the Modify button and on the next screen look for the ‘Index Partition’ (you may have more than one)
  • Click on the ‘Query Component 0’ link and Edit Properties
  • The field called ‘Location of Index’ contains the root location for the thesaurus files

image

Looking in that folder, you should find folders that look something like this:

image

As noted above, the thesaurus files you want to work with are in the GUID-query-0\Config folder. In my case above, it’s E:\Data\SearchIndex\Office Server\Applications\0f78bae4-05b9-417f-b533-43326409dfcc-query-0\Config

Happy equivalency!

One side note: it boggles my mind that there is no UI to manage synonyms in the thesaurus, but there you go.

Setting a Rich Text Column in a SharePoint Form with jQuery – SharePoint 2010

I have gotten several questions on an older post of mine titled Setting a Rich Text Column in a SharePoint Form with jQuery from a guy named Travis. He’s been struggling to try to use the script in the post to get at the text in a Rich Text Editor (RTE) in SharePoint 2007.

I just took a look at an RTE in SharePoint 2010 in both IE and Firefox. The markup for it is considerably different than it was in SharePoint 2007, and looking at my older post, I can tell from the screenshots that I was in 2007. The good thing is that it looks identical in both browsers in 2010, which wasn’t usually the case in 2007.

Here’s what the column looks like in SharePoint 2010 with the same text typed into it as in the previous example:

9-11-2013 12-56-16 PM

And here’s what the markup looks like in IE10:

9-11-2013 1-04-32 PMThe script in the older post shouldn’t work in any browser, since the markup is different. Rather than a textarea, the typed text goes into a div. It was pretty easy to select the textarea in 2007 because it had its title attribute set to the DisplayName of the column. In 2010, there’s no such obvious “hook” to select on.

Instead, we have to fall back to looking for the column name in the comment which is above each column’s control on the page. To recap, each cell in the left column where the names of the columns show has the CSS class ms-formlabel applied to it. In the right column, where the editing controls are, each cell has the class ms-formbody applied to it.

To find the RTE in the page, we have to loop through all of the ms-formbody cells to find the right one. I do this in SPServices for some things as well, because SharePoint is very inconsistent when it comes to providing a good way to select form elements.

Here’s my findFormField function from SPServices:

// Finds the td which contains a form field in default forms using the comment which contains:
//  <!--  FieldName="Title"
//    FieldInternalName="Title"
//    FieldType="SPFieldText"
//  -->
// as the "anchor" to find it. Necessary because SharePoint doesn't give all field types ids or specific classes.
function findFormField(columnName) {
  var thisFormBody;
  // There's no easy way to find one of these columns; we'll look for the comment with the columnName
  var searchText = RegExp("FieldName=\"" + columnName.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&") + "\"", "gi");
  // Loop through all of the ms-formbody table cells
  $("td.ms-formbody, td.ms-formbodysurvey").each(function() {
    // Check for the right comment
    if(searchText.test($(this).html())) {
      thisFormBody = $(this);
      // Found it, so we're done
      return false;
    }
  });
  return thisFormBody;
} // End of function findFormField

So the script to get the HTML value of the RTE – assuming we have the function above available to us – is:

var thisFieldHtml = findFormField("System Description").find(".ms-rtestate-write div").html();
alert(thisFieldHtml);

Unfortunately (fortunately?) SharePoint keeps track of where our cursor is at any given time during the editing process by maintaining two spans inside the div.

<span id="ms-rterangecursor-start"></span><span id="ms-rterangecursor-end"></span>

Here’s what it looks like in an alert when I’ve just typed “big “:

9-11-2013 1-33-09 PMIf we highlight a section of text, then that highlighted text is contained within the two spans. Here I’ve highlighted the word “over”:

<span id="ms-rterangecursor-start"></span>over<span id="ms-rterangecursor-end"></span>

So if we want to get the HTML value of the RTE without the editing spans, we need one more line of code to remove them:

var thisFieldHtml = findFormField("System Description").find(".ms-rtestate-write div");
thisFieldHtml.find("#ms-rterangecursor-start, #ms-rterangecursor-end").remove();
alert(thisFieldHtml.html());

If we just want the text value of the RTE, then we don’t need to bother stripping out the spans:

var thisFieldText = findFormField("System Description").find(".ms-rtestate-write div").text();
alert(thisFieldText);

Note: I’ve also tried this in SharePoint 2013. There, the artificial surrounding div which SharePoint wrapped the Rich Text in with the earlier version is not present, so we don’t need to look inside it:

var thisFieldText = findFormField("Reason for Nomination").find(".ms-rtestate-write").text();
alert(thisFieldText);

Travis, I hope this helps!

Comparing SPServices 2013.01 Calls with Async vs. Promises Methods

SPServices 2013.01 is almost ready for release and I’m hoping that when you see the coolness here that you’ll want to get your hands on the beta to do a little testing with it.

I’ve put together a pretty simple demo page to show how great it can be to use jQuery .Deferred objects (aka promises) to retrieve data with SPServices versus the asynchronous way we’ve tended to use until now. The demo page is running on my old WSS 3.0 site hosted at FPWeb. Unfortunately, I don’t have a public-facing SharePoint 2013 instance anywhere yet.

The idea here is pretty simple. I wanted to keep the complexity of the code pretty low and also to use as much common code as possible between the two methods so that the real apples to oranges comparison is the async method versus the promises method.

I’ve set up three Announcements lists in a single site: Corporate Announcements, Finance Announcements, and HR Announcements. Each list has three announcements in it. I even got to use Bacon Ipsum to fill in some dummy content.

The demo page has two buttons on it: one for async and one for promises. When you push one of the buttons, the script loops through the array containing information about the three lists, retrieves the three items from each of the lists using the chosen method, and displays them on the page. There’s a slightly different version of the code in each of the functions (getRecentAnnouncementsAsync and getRecentAnnouncementsPromises) that run in each case.

The getRecentAnnouncementsAsync function runs the way we’re all used to using SPServices. The async option is set to false and processing of the returned data happens inside the completefunc.

In the promises version, the async option isn’t set, so it has the default of true (we don’t wait for the returned data, but continue processing). The promise returned from each call to GetListItems is put into an array, and the line $.when.apply($, announcementsPromises).done(...) is where the check for all three promises to be complete happens. When that occurs, the data for the three calls is processed.

Here’s the code:

// Show the most recent Annoucements from three lists in the current site
var announcementsPerList = 3;
var lists = [{
    name : "Corporate Announcements"
  }, {
    name : "HR Announcements"
  }, {
    name : "Finance Announcements"
  }
];
var camlQuery = "<Query><OrderBy><FieldRef Name='Created' Ascending='FALSE'/></OrderBy></Query>";
var camlViewFields = "<ViewFields>" +
  "<FieldRef Name='ID' />" +
  "<FieldRef Name='Title' />" +
  "<FieldRef Name='Body' />" +
  "<FieldRef Name='Author' />" +
  "<FieldRef Name='Created' />" +
  "<FieldRef Name='FileDirRef' />" +
  "</ViewFields>";
var out;

$(document).ready(function () {

  //$("#ctl00_site_share_button").hide();
  $("input[value='async']").click(function () {

    getRecentAnnouncementsAsync();

  });

  $("input[value='promises']").click(function () {

    getRecentAnnouncementsPromises();

  });

});

//++++++++++++++++++++++++++++++++++++

function getRecentAnnouncementsAsync() {

  var recentAnnouncementsContainer = $("#demo-recent-announcements");
  recentAnnouncementsContainer.empty();
  recentAnnouncementsContainer.addClass("demo-loading");

  out = "<table>";
  announcementHeader();

  for (var i = 0; i < lists.length; i++) {

    $().SPServices({
      operation : "GetListItems",
      async : false,
      listName : lists[i].name,
      CAMLQuery : camlQuery,
      CAMLViewFields : camlViewFields,
      CAMLRowLimit : announcementsPerList,
      completefunc : function (xData) {

        // List header
        out += "<tr><td class='demo-section-header' colspan='99'>" + lists[i].name + "</td></tr>";

        $(xData.responseXML).SPFilterNode("z:row").each(function (itemNum) {

          processAnnouncement(i, $(this));

        });

      }
    });

  }

  out += "</table>";
  $("#demo-recent-announcements").removeClass("demo-loading");
  recentAnnouncementsContainer.html(out);

}
function getRecentAnnouncementsPromises() {

  var recentAnnouncementsContainer = $("#demo-recent-announcements");
  recentAnnouncementsContainer.empty();
  var announcementsPromises = [];
  recentAnnouncementsContainer.addClass("demo-loading");

  out = "<table>";
  announcementHeader();

  for (var i = 0; i < lists.length; i++) {

    announcementsPromises[i] = $().SPServices({
        operation : "GetListItems",
        listName : lists[i].name,
        CAMLQuery : camlQuery,
        CAMLViewFields : camlViewFields,
        CAMLRowLimit : announcementsPerList
      });

  }

  // When all of the promises are fulfilled...
  $.when.apply($, announcementsPromises).done(function () {
    for (var i = 0; i < lists.length; i++) {

      // List header
      out += "<tr><td class='demo-section-header' colspan='99'>" + lists[i].name + "</td></tr>";

      $(announcementsPromises[i].responseXML).SPFilterNode("z:row").each(function (itemNum) {

        processAnnouncement(i, $(this));

      });

    }

    out += "</table>";
    $("#demo-recent-announcements").removeClass("demo-loading");
    recentAnnouncementsContainer.html(out);

  });

}

function getNow() {

  return new Date();

}

function announcementHeader() {

  out += "<tr class='ms-WPHeader'><td colspan='99'><h3 class='ms-standardheader ms-WPTitle'>Recent Announcements as of " + getNow() + "</h3></td></tr>";
  out += "<tr>" +
  "<th class='ms-vh2'>Title</th>" +
  "<th class='ms-vh2'>Body</th>" +
  "<th class='ms-vh2'>Created By</th>" +
  "<th class='ms-vh2'>Created</th>" +
  "</tr>";

}

function processAnnouncement(i, item) {

  out += "<tr>";

  // Title as a link to the announcement
  var thisLink = item.attr("ows_FileDirRef").split(";#");
  out += "<td class='ms-vb demo-note-details' style='width:40%;'>" +
  "<a href='/" + thisLink[1] + "/DispForm.aspx?ID=" + thisLink[0] + "&Source=" + location.href + "' data-announcement-id='" + item.attr("ows_ID") +
  "' data-list='" + lists[i].name + "' >" +
  item.attr("ows_Title") +
  "</a>" +
  "</td>";

  // Body
  var thisBody = item.attr("ows_Body");
  out += "<td class='ms-vb'>" + ((typeof thisBody !== "undefined") ? thisBody : "NA") + "</td>";

  // Author as a link to the User Profile
  var thisAuthor = item.attr("ows_Author").split(";#");
  out += "<td class='ms-vb' style='width:15%;'>" +
  "<a href='/_layouts/userdisp.aspx?ID=" + thisAuthor[0] + "' onclick='GoToLink(this);return false;'>" + thisAuthor[1] + "</a>" +
  "</td>";

  // Created date/time
  out += "<td class='ms-vb' style='width:15%;'>" + item.attr("ows_Created") + "</td>";

  out += "</tr>";

}

Note that I’m using the same version of SPServices here. SPServices 2013.01 can be used as you’re used to using it and everything will work as you are used to. If you are making multiple calls and the promises method would make more sense (mainly in terms of efficiency), then you can take advantage of it.

There are two big benefits of using the promises method:

  • If there are multiple calls to the Web Services that can logically happen concurrently (there’s less benefit if there is only one call), the promises method allows that to happen.
  • In the promises method, the browser doesn’t get locked up like it does with the async method. Setting async to true means that the browser is going to wait for the results to come back from the call before it will allow anything else to happen. With promises, the processing continues even when the results aren’t available. (This can take some getting used to, and I’ll undoubtedly do more posts about it.)

I’ve tested the code in SharePoint 2013 on Office365 and WSS 3.0 at FPWeb, which are about the most opposite ends of the spectrum that we can try these days. The code that I tested is *exactly* the same in the two environments. I simply copied and pasted the file from SharePoint 2013 where I started into the WSS 3.0 environment. How’s that for consistency, even on a downgrade? Oh, if Microsoft’s code only did that!

Using Firebug, I’ve captured information from the two methods on the Net tab for the XHR traffic.

Here are the results in SharePoint 2013:

Async

image

Promises

image

As you can see the total elapsed time decreased from about 503ms to 107ms. These results are fairly consistent. I haven’t tried to build any fancy instrumentation around it, but if I click the buttons repeatedly, I get consistent results in both cases.

Here are the results from WSS 3.0:

Asynch

image

Promises

image

Here, the total elapsed time decreased from about 409ms to 130ms.

In both versions of SharePoint, you can quite obviously see the difference. With the async method, the three calls happen in order, whereas with the promises method, they happen pretty much concurrently. (Even with promises, there’s a small lag due to the processing to set up the calls.)

But don’t take my word for it. go and grab your own copy of the