Organizing asynchronous calls to google.script.run

There's a lot of questions on the the Google Apps Script forum about handling asynchronous calls from an HtmlService client back to the Server. This is done using google.script.run - but it's important to realize that this is an asynchronous call. This means that code coming afterwards will be executed before it's actually finished. For those used to Server side Apps Script it can be confusing. In this post I'll show how to better orchestrate these kind of calls. 

Elsewhere on my site you'll see how I handle this with an eye to reusing the same code on the client and the server using the Provoke namespace, but for this tutorial, I'll go back to basics with google.script.run. 

Server side functions

Let's say that you have 2 server side functions you want to call from client side. One that gets the name of all the sheets in a spreadsheet, and then gets the headings in a given sheet. The simplest way to do this would be to create a server side function that did both at the same time, however you may want to first get the list, then do something client side to choose the sheet you want, then get the headings. 

First though let's simply look at getting the sheet names in a spreadsheet.

Server side, this function is waiting to be invoked
function getSheetsInBook (id) {

  return SpreadsheetApp
  .openById(id)
  .getSheets()
  .map(function (d) {
    return d.getName();
  });
}

Client side calls

And we'll call it like this from client side. It's important to call both the .withSuccessHandler and .withFailureHandler methods to define a function to execute in either circumstance. Here I'm simply showing the result, or the error.
 function getStuffFromServer () {

      // it's asynchronous
      google.script.run
      
      // deal with the result
      .withSuccessHandler (function (result) {
        
        // will be called when done
        showResult (result,"sheets");
      })
      
      // deal with error
      .withFailureHandler (function (error) {
      
        // will be called on failure
        showError (error);
      
      })
      
      // the function to run and arguments to it
      .getSheetsInBook (id);
      
      // note that when we get here, it hasn't run yet
      // if you have something dependent on completion
      // it goes in the withSuccessHandler callback
      
    }

I've linked this function with the getSheets button, and we get this result in the Sheets div.

Imagine now that we needed to select a sheet and make a second call to another server side function. It would need to happen inside the function activated by .withSuccessHandler. Here's the second server side function
// get a single row of data
function getDataFromRow (id , sheetName, rowOffset) {

  return SpreadsheetApp
  .openById(id)
  .getSheetByName(sheetName)
  .getDataRange()
  .offset(rowOffset, 0, 1)
  .getValues()[0];
}

So now we have a second google.script.run pattern inside the first.
 function getDependentStuffFromServer () {
    
      var id = '1wC0zA-CkBu78VbYWZ4tMe0iVj5Buu6dJegXvD1DhnZk';
      
      // it's asynchronous
      google.script.run
      
      // deal with the result
      .withSuccessHandler (function (result) {
        
        // will be called when done
        showResult (result,"sheets");

        // now do something else
        google.script.run
        .withSuccessHandler (function (result) {
          showResult (result,"headings");
          showError ("all done");
        })
        .withFailureHandler (function (error) {
          showError (error);
        })
        
        // we'll get the headings of the first sheet
        .getDataFromRow ( id , result[0] , 0 );
        
        
      })
      
      // deal with error
      .withFailureHandler (function (error) {
      
        // will be called on failure
        showError (error);
      
      })
      
      // the function to run and arguments to it
      .getSheetsInBook (id);
      
      // note that when we get here, it hasn't run yet
      // if you have something dependent on completion
      // it goes in the withSuccessHandler callback
      
    }

Which I'll attach to the get headings button

Organizing with promises

But it's starting to get a little messy. If there were other functions following on, we'd end up with a real mess - so here's an alternative approach - using promises to tame the spaghetti, so all that could be written as simply as this
    function orchestrateWithPromises () {
    
      return promiseRun ('getSheetsInBook' , id)
      .then (function (result) {
        showResult (result, "sheets");
        return promiseRun ('getDataFromRow',  id , result[0] , 0);
      })
      .then (function (result) {
        showResult (result, "headings");
      });
    
    }

So lets look at this generalized promiseRun function which can be used to replace the google.script.run pattern.
    // using promises instead
    function promiseRun (func) {
      
      // this is a trick to convert the arguments array into an array, and drop the first one
      var runArgs = Array.prototype.slice.call(arguments).slice(1);
      
      return new Promise (function (resolve, reject) {
        google.script.run
        .withSuccessHandler (function (result) {
          resolve (result);
        })
        .withFailureHandler (function (error) {
          reject (error);
        })
        [func].apply (this , runArgs) ;
        
      })
    
    }

Creating a promise

When you execute an asynchronous request, it completes by executing a call back. A promise allows you to wrap up an asynchronous request and its result in a convenient package that you can use to easily sequence dependent processes. A promise is either resolved (it finished successfully), rejected (it failed) or pending - meaning it's still being executed. 

Many functions return promises, and they do that by creating a promise object. The argument to that promise is a function that does the work you want to "promisify". 

That function receives 2 arguments
  • resolve - a function that marks the promise as resolved
  • reject - a function that marks the promise as rejected
That means I can wrap google.script.run in  a function callback to a new Promise object, and my .withSuccessHandler and .withFailureHandler functions will call resolve or reject appropriately. I also pass the result and error as arguments to both resolve and reject. These are preserved in the promise so they can be retrieved later. 

Arguments

Since this is a generalized function it needs to be able to receive the name of the serverside function, but also there may be arguments to deal with, so we need to find a way of passing on the arguments. JavaScript arguments arrive in a function in a special object called "arguments". This is a kind of an array, but it doesn't have the methods available that you find in a normal array.

Arriving in my function will be at least the name of the server side function, but any number of some others, so the first task is to strip off the function name from the arguments object - that's what's going on here. In summary - because arguments doesn't have a .slice method on its own, I can "borrow" the slice method from theArray prototype like this.
 var runArgs = Array.prototype.slice.call(arguments).slice(1);

Later on, I have the opposite problem- meaning that I need to convert this array back to a an argument type object. I can use the apply method of the function to pass the arguments to the function.
[func].apply (this , runArgs) ;

Error handling

If an error happens server side, the function passed to .withFailureHandler is executed, so in the generalized promiseRun function, the .withFailure handler will call the reject() function to indicate a failed promise. 
    // using promises instead
    function promiseRun (func) {
      
      // this is a trick to convert the arguments array into an array, and drop the first one
      var runArgs = Array.prototype.slice.call(arguments).slice(1);
      
      return new Promise (function (resolve, reject) {
        google.script.run
        .withSuccessHandler (function (result) {
          resolve (result);
        })
        .withFailureHandler (function (error) {
          reject (error);
        })
        [func].apply (this , runArgs) ;
        
      })
    
    }

Putting it all together

Here's the final thing
    function orchestrateWithPromises () {
    
      return promiseRun ('getSheetsInBook' , id)
      .then (function (result) {
        showResult (result, "sheets");
        return promiseRun ('getDataFromRow',  id , result[0] , 0);
      })
      .then (function (result) {
        showResult (result, "headings");
      });
    
    }

Notice that no matter how many server side functions are dependent on each other, they can simply be chained together using .then and you can be sure they will run in the correct order, as the function passed to .then is not executed until the Promise is resolved, and here is the result.

Error handling

We've looked at the .then method of a Promise which is executed following a successful resolution. Promises also have a .catch method which is executed following a reject. This is the mechanism for promise error handling. Another benefit is that you don't need to handle an error at every level, which can be another source of spaghetti. Although I have a few Promises in this process, I'm only going to bother handling this at the very top level, so here's how I assign this entire process to a button.
      prom.addEventListener ("click" , function (e) {
        orchestrateWithPromises()
        .then (function () {
          showError ("all done by promises");
        })
        .catch (function(error) {
          showError (error);
        });
      });

Parallel orchestration


So far we've looked at sequential operations. Promises also make it easy to control operations that can happen in parallel. That'll be in the next post on this subject, which you'll find here Organizing parallel streams of server calls with google.script.run promises

You want to learn Google Apps Script?

Learning Apps Script, (and transitioning from VBA) are covered comprehensively in my my book, Going Gas - from VBA to Apps script, available All formats are available now from O'Reilly,Amazon and all good bookshops. You can also read a preview on O'Reilly

If you prefer Video style learning I also have two courses available. also published by O'Reilly.
Google Apps Script for Developers and Google Apps Script for Beginners.





For more like this, see Google Apps Scripts snippets. Why not join our forumfollow the blog or follow me on twitter to ensure you get updates when they are available. 





Comments