Apps Script doesn’t do asynchronous – so what’s this all about?

Since the move to v8, Apps Script supports the Promises as well as the related async/await syntax. Normally we don’t have to worry about all that since all Apps Script services are synchronous. Yet we do know that server side, plenty of asynchronous things are happening. So there is clearly a mechanism for making asynchronous things synchronous.

In an imminent article I’ll be covering my port of the excellent pdf-lib library to Apps Script. Before I get to that I wanted to do a bit of background on handling Promises in Apps Script. The methods in these kind of Node ports to Apps Script libraies often return Promises. In Apps Script you’ll have to deal with the complication of async without getting the benefits. This article is to provide some background for the use of such libraries.

What is a Promise

An asynchronous operation (for example a fetch to a url in Node), won’t return the result right away. Instead it returns a ‘promise to return the result’ at some future time. This allows you to get on with something else in the meantime. Often you can have many outstanding unresolved promises that all return their results asynchronously and unpredictably.

Of course, in Apps Script that doesn’t happen as each line of code runs serially and needs to complete before the next one can run. Nevertheless the behavior is as if it really was an asynchronous execution. Here’s how to get result of a promise.

const somethingThatReturnsAPromise = () => {
return Promise.resolve ('hello world')
}

const promise = somethingThatReturnsAPromise()

promise.then (result=> {
// do something with the result
})
Get the result of a promise

You can see the problem here that ‘result’ is only available inside the .then() method of the promise. Anything that comes after the promise will not be able to make use of its result.

async/await

A less messy way of dealing with a promise is using async/await. This will give access to the result in the code following the promise execution.

const func = async () => {
const result = await somethingThatReturnsAPromise()
// do something with result
console.log (result)
// this will be returned as a promise
// because the function is async
return result
}
Using async/await

However note that the function includes the async tag – you can’t use await outside of an async function. Anything that is returned from an async function gets returned as a promise. You have to go through all that again in a calling function

const func = async () => {
const result = await somethingThatReturnsAPromise()
// do something with result
console.log (result)
// this will be returned as a promise
// because the function is async
return result
}
const func2 = () => {
// this will return a promise to result,not result itself
const result = func()

// we would have to do this to get actual result
result.then(value=> console.log (value))
}
// or we could use await/async again
const func3 = async () => {

// this will return a promise to result,not result itself
const result = await func()

// we would have to do this to get actual result
console.log (result)

}

Minimizing complication to code flow

So you can see that promises come with some baggage, regardless of whether you are using the simplified aync/await or the regular promise methods.

Using classes to disguise some of the complication can help, but there are also issues with creating instances of classes that contain asynchronicity. The rest of this article will look at workarounds for async class instantiation.

Some helpers

Here’s a couple of helpers we’ll use in the later examples.

// a function that waits some period of time then returns some data as a promise
const delay = (ms) => new Promise(resolve => {
const startedAt = new Date().getTime()
Utilities.sleep(ms)
const finishedAt = new Date().getTime()
resolve({
startedAt,
finishedAt,
ms,
duration: finishedAt - startedAt
})
})
// use this to check if something is a promise
const isPromise = (ob) => ob instanceof Promise
// we'll use this to log the object type
const logIsPromise = (ob, name = 'object') =>
console.log(`${name} is ${isPromise(ob) ? '' : ' not '} a promise'`)
Helpers

Let’s validate all the correct types are being generated

const t1 = () => {
// p1 is a promise
const p1 = delay(1000)
// in apps script - we don't get here until delay is resolved
// in javascript we would get here immediately
// because app script Utilities.sleep is blocking
console.log('delay was invoked', new Date().getTime())
logIsPromise(p1, 'p1')

// p2 is still a promise even though its resolved
const p2 = p1.then(result => {
// result is not a promise
logIsPromise(result, 'result')
console.log(result)
return result
})
logIsPromise (p2, 'p2')

}
// logs ...
3:04:35 PM Info delay was invoked 1718546675747
3:04:35 PM Info p1 is a promise'
3:04:35 PM Info p2 is a promise'
3:04:35 PM Info result is not a promise'
3:04:35 PM Info { startedAt: 1718546674743,
finishedAt: 1718546675744,
ms: 1000,
duration: 1001 }
t1

You can see that the time of the execution of the logger after calling the delay function is just after the delay function finished executing. This shows that the Apps Script sleep function is indeed blocking. The JavaScript equivalent (setTimeout) is asynchronous and control would have passed on immediately.

using async

Here’s the same thing, this time using await/async

const a1 = async () => {
// p1 is no longer a promise
// but we need to make the function async, so we can use await syntax
const p1 = await delay(1000)
logIsPromise(p1, 'awaited object')
console.log(p1)
// although p1 is no longer a promise, because the function is async it'll turn the return into a promose
return p1
}
// result
3:11:56 PM Info awaited object is not a promise'
3:11:56 PM Info { startedAt: 1718547115397,
finishedAt: 1718547116399,
ms: 1000,
duration: 1002 }
a1

This time the result from await delay is not a promise, but the actual result

Result from an async function will be a promise

const a2 = () => {
// p1 is again a promise
const p1 = a1()
logIsPromise(p1, 'returned object')
// so we'd need to use an async function to undo
}
3:18:42 PM Info returned object is a promise'
3:18:42 PM Info awaited object is not a promise'
3:18:42 PM Info { startedAt: 1718547521111,
finishedAt: 1718547522113,
ms: 1000,
duration: 1002 }
a2

It’s interesting to notice that the logging is not completely serial. The log from the called function ‘awaited object’ happens after the log ‘returned object’. Clearly there is some small kind of asynchronicity happening in the Apps Script universe.

Creating a class instance

We can create a where one of the properties is asynchronous. But we’d need to await the result on access.

class c3 {
constructor(ms) {
this._constructTime = delay(ms)
}
get constructTime() {
return this._constructTime
}
}
const a3 = () => {
const c = new c3(1000)
logIsPromise(c.constructTime, 'construct time property')
c.constructTime.then(constructTime=>console.log(constructTime))
}
// result
3:32:51 PM Info construct time property is a promise'
3:32:51 PM Info { startedAt: 1718548370769,
finishedAt: 1718548371771,
ms: 1000,
duration: 1002 }
a3

Using an IEF in the constructor

class c4 {
constructor(ms) {
// this will still be a promise
this._constructTime = (async () => await delay(ms))()
}
get constructTime() {
return this._constructTime
}
}

const a4 = () => {
const c = new c4(1000)
logIsPromise(c.constructTime, 'construct time')
c.constructTime.then(constructTime=>console.log(constructTime))
}
// results
3:36:16 PM Info construct time is a promise'
3:36:16 PM Info { startedAt: 1718548575898,
finishedAt: 1718548576899,
ms: 1000,
duration: 1001 }
a4

In this case, we’re awaiting the result of the promise in the constructor. However as previously explained an async function will convert a plain result into a promise. We still end up with a promise being returned when we access the constructTime property.

Returning the instance as a promise

Another approach could be to return the instance from the constructor as a promise. But it’s an odd quirk that you can’t appear to use await/async with a promise to a constructor, although the promise methods do work.

class c5 {
constructor(ms) {
// can we make the constructot async
return delay(ms).then((result) => {
this._constructTime = result
return this
})
}
get constructTime() {
return this._constructTime
}
}

const a5 = () => {
const c = new c5(1000)
// we've returned a promise instead of an instance of the class
logIsPromise(c, 'instance of c5')
// this is undefined, because c is actually a promise
logIsPromise(c.constructTime, 'construct time')

// this works, but again we have the constructed value in an async box
c.then(cinstance => {
logIsPromise(cinstance, 'instance of c5')
console.log(cinstance.constructTime)
})

// but await doesn't work with classes that return promises
/*
const c2 = new c5(1000)
(async ()=> {
const cinstance = await c2
logIsPromise(cinstance,'instance of c5')
console.log(cinstance)
})()
*/
}
// result
3:39:34 PM Info instance of c5 is a promise'
3:39:34 PM Info construct time is not a promise'
3:39:34 PM Info instance of c5 is not a promise'
3:39:34 PM Info { startedAt: 1718548773393,
finishedAt: 1718548774394,
ms: 1000,
duration: 1001 }
c5

Using a factory function

This seems to be the cleanest approach. In this case, the constructor doesn’t really do anything and instead the initialization is delegated to an async function.

class c6 {
// this constructor doesnt do anything
constructor() {

}
// we delegate the work to a factory function
async factory(ms) {
this._constructTime = await delay(ms)
return this
}
get constructTime() {
return this._constructTime
}
static async build(ms) {
const c = new c6()
return c.factory(ms)
}
}

const a6 = async () => {
const c = await c6.build(1000)
// we've returned a promise instead of an instance of the class
logIsPromise(c, 'instance of c6')
// this is undefined, because c is actually a promise
logIsPromise(c.constructTime, 'construct time')
console.log(c.constructTime)

}

// results
3:46:30 PM Info instance of c6 is not a promise'
3:46:30 PM Info construct time is not a promise'
3:46:30 PM Info { startedAt: 1718549189989,
finishedAt: 1718549190991,
ms: 1000,
duration: 1002 }
a6

This approach consists of an initialization method (factory) and a static function (build) which is used to create a new instance.

In other words instead of using

new c6(ms) 

to create an instance of the class, we can use

await c6.build (ms)

Next

In future articles which have to address this topic, I’ll be using the await class.build(ms) approach to create instances which would otherwise need to have an asynchronous constructor.

Links

Manipulating PDFS in Apps Script