'Asynchronous execution of a function App Script

I've been digging around, and I'm not able to find references or documentation on how I can use Asynchronous Functions in Google App Script, I found that people mention It's possible, but not mention how...

Could someone point me in the right direction or provide me with an example? Promises, Callbacks, or something, that can help me with this.

I have this function lets call it foo that takes a while to execute (long enough that It could time out an HTTP call).

What I'm trying to do Is to refactor it, in a way that it works like this:

function doPost(e) {
    // parsing and getting values from e
    var returnable = foo(par1, par2, par3);
      return ContentService
             .createTextOutput(JSON.stringify(returnable))
             .setMimeType(ContentService.MimeType.JSON);
}

function foo(par1, par2, par3) {
    var returnable = something(par1, par2, par3); // get the value I need to return;

    // continue in an Async way, or schedule execution for something else
    // and allow the function to continue its flow
    /* async bar(); */

    return returnable;
}

Now I want to realize that bit in foo because It takes to long and I don't want to risk for a time out, also the logic that occurs there it's totally client Independent, so It doesn't matter, I just need the return value, that I'll be getting before.

Also, I think It's worth mentioning that this is deployed in Google Drive as a web app.

It's been long since this, but adding some context, at that moment I wanted to scheduled several things to happen on Google Drive, and It was timing out the execution, so I was looking for a way to safely schedule a job.



Solution 1:[1]

  • You want to execute functions by the asynchronous processing using Google Apps Script.
  • You want to run the functions with the asynchronous processing using time trigger.

If my understanding is correct, unfortunately, there are no methods and the official document for directly achieving it. But as a workaround, that can be achieved by using both Google Apps Script API and the fetchAll method which can work by asynchronous processing.

The flow of this workaround is as follows.

  1. Deploy API executable, enable Google Apps Script API.
  2. Using fetchAll, request the endpoint of Google Apps Script API for running function.
    • When several functions are requested once, those work with the asynchronous processing by fetchAll.

Note:

  • I think that Web Apps can be also used instead of Google Apps Script API.
  • In order to simply use this workaround, I have created a GAS library. I think that you can also use it.
  • In this workaround, you can also run the functions with the asynchronous processing using time trigger.

References:

If I misunderstand your question, I'm sorry.

Solution 2:[2]

There is another way to accomplish this.

You can use time-based one-off triggers to run functions asynchronously, they take a bit of time to queue up (30-60 seconds) but it is ideal for slow-running tasks that you want to remove from the main execution of your script.

// Creates a trigger that will run a second later
ScriptApp.newTrigger("myFunction")
  .timeBased()
  .after(1)
  .create();

There is handy script that I put together called Async.gs that will help remove the boilerplate out of this technique. You can even use it to pass arguments via the CacheService.

Here is the link:

https://gist.github.com/sdesalas/2972f8647897d5481fd8e01f03122805

// Define async function
function runSlowTask(user_id, is_active) {
  console.log('runSlowTask()', { user_id: user_id, is_active: is_active });
  Utilities.sleep(5000);
  console.log('runSlowTask() - FINISHED!')
}

// Run function asynchronously
Async.call('runSlowTask');

// Run function asynchronously with one argument
Async.call('runSlowTask', 51291);

// Run function asynchronously with multiple argument
Async.call('runSlowTask', 51291, true);

// Run function asynchronously with an array of arguments
Async.apply('runSlowTask', [51291, true]);

// Run function in library asynchronously with one argument
Async.call('MyLibrary.runSlowTask', 51291);

// Run function in library asynchronously with an array of arguments
Async.apply('MyLibrary.runSlowTask', [51291, true]);

Solution 3:[3]

With the new V8 runtime, it is now possible to write async functions and use promises in your app script.

Even triggers can be declared async! For example (typescript):

async function onOpen(e: GoogleAppsScript.Events.SheetsOnOpen) {
    console.log("I am inside a promise");
    // do your await stuff here or make more async calls
}

To start using the new runtime, just follow this guide. In short, it all boils down to adding the following line to your appsscript.json file:

{
  ...
  "runtimeVersion": "V8"
}

Solution 4:[4]

Based on Tanaike's answer, I created another version of it. My goals were:

  • Easy to maintain
  • Easy to call (simple call convention)

tasks.gs

class TasksNamespace {
  constructor() {
    this.webAppDevUrl = 'https://script.google.com/macros/s/<your web app's dev id>/dev';
    this.accessToken = ScriptApp.getOAuthToken();
  }

  // send all requests
  all(requests) {
    return requests
    .map(r => ({
      muteHttpExceptions: true,
      url: this.webAppDevUrl,
      method: 'POST',
      contentType: 'application/json',
      payload: {
        functionName: r.first(),
        arguments: r.removeFirst()
      }.toJson(),
      headers: {
        Authorization: 'Bearer ' + this.accessToken
      }
    }), this)
    .fetchAll()
    .map(r => r.getContentText().toObject())
  }

  // send all responses
  process(request) {
    return ContentService
    .createTextOutput(
      request
      .postData
      .contents
      .toObject()
      .using(This => ({
        ...This,
        result: (() => {
          try {
            return eval(This.functionName).apply(eval(This.functionName.splitOffLast()), This.arguments) // this could cause an error
          }
          catch(error) {
            return error;
          }
        })()
      }))
      .toJson()
    )
    .setMimeType(ContentService.MimeType.JSON)
  }
}

helpers.gs

  // array prototype

  Array.prototype.fetchAll = function() {
    return UrlFetchApp.fetchAll(this);
  }

  Array.prototype.first = function() {
    return this[0];
  }

  Array.prototype.removeFirst = function() {
    this.shift();
    return this;
  }

  Array.prototype.removeLast = function() {
    this.pop();
    return this;
  }


  // string prototype

  String.prototype.blankToUndefined = function(search) {
    return this.isBlank() ? undefined : this;
  };    

  String.prototype.isBlank = function() {
    return this.trim().length == 0;
  }

  String.prototype.splitOffLast = function(delimiter = '.') {
    return this.split(delimiter).removeLast().join(delimiter).blankToUndefined();
  }

  // To Object - if string is Json
  String.prototype.toObject = function() {
    if(this.isBlank())
      return {};
    return JSON.parse(this, App.Strings.parseDate);
  }

  // object prototype

  Object.prototype.toJson = function() {
    return JSON.stringify(this);
  }

  Object.prototype.using = function(func) {
    return func.call(this, this);
  }

http.handler.gs

function doPost(request) {
  return new TasksNamespace.process(request);
}

calling convention

Just make arrays with the full function name and the rest are the function's arguments. It will return when everything is done, so it's like Promise.all()

var a =  new TasksNamespace.all([
    ["App.Data.Firebase.Properties.getById",'T006DB4'],
    ["App.Data.External.CISC.Properties.getById",'T00A21F', true, 12],
    ["App.Maps.geoCode",'T022D62', false]
  ])

return preview

[ { functionName: 'App.Data.Firebase.Properties.getById',
    arguments: [ 'T006DB4' ],
    result: 
     { Id: '',
       Listings: [Object],
       Pages: [Object],
       TempId: 'T006DB4',
       Workflow: [Object] } },
...
]

Notes

  • it can handle any static method, any method off a root object's tree, or any root (global) function.
  • it can handle 0 or more (any number) of arguments of any kind
  • it handles errors by returning the error from any post

Solution 5:[5]

// First create a trigger which will run after some time 
ScriptApp.newTrigger("createAsyncJob").timeBased().after(6000).create();

/* The trigger will execute and first delete trigger itself using deleteTrigger method and trigger unique id. (Reason: There are limits on trigger which you can create therefore it safe bet to delete it.)
Then it will call the function which you want to execute.
*/
function createAsyncJob(e) {
  deleteTrigger(e.triggerUid);
  createJobsTrigger();
}

/* This function will get all trigger from project and search the specific trigger UID and delete it.
*/
function deleteTrigger(triggerUid) {
  let triggers = ScriptApp.getProjectTriggers();
  triggers.forEach(trigger => {
    if (trigger.getUniqueId() == triggerUid) {
      ScriptApp.deleteTrigger(trigger);
    }
  });
}

Solution 6:[6]

While this isn't quite an answer to your question, this could lead to an answer if implemented.

I have submitted a feature request to Google to modify the implementation of doGet() and doPost() to instead accept a completion block in the functions' parameters that we would call with our response object, allowing additional slow-running logic to be executed after the response has been "returned".

If you'd like this functionality, please star the issue here: https://issuetracker.google.com/issues/231411987?pli=1

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Tanaike
Solution 2
Solution 3 smac89
Solution 4
Solution 5 Pandey Harsh Sunil
Solution 6 Adam Eisfeld