I’m writing some JavaScript tutorial stuff at the moment, so there hasn’t been too much activity here for a couple of months, but I thought I’d do a series of posts to share a few little tips I’ve found useful when creating this tutorial material.

In the tutorials, I’m asking the learner to write some code to incorporate in a mainly already written App, so I’ve no control over what they write, but I do want to provide some feedback on where it’s gone wrong

Getting Code in the right order

You can’t really dictate the order Apps Script loads your scripts in, so it can be difficult to avoid accessing classes and namespaces before they are defined. Here’s how I do it.

Iffys

This is a special problem for Immediately invoked function expressions (IIFE) , or ‘iffys’ used to make namespaces. These are executed when the scripts are loaded, so you have absolutely no control over these.

Say you have 4 seperate scripts files like this, you run the risk of trying to access things before they are defined

//script file 1
const En = (() => {
return {
greeter: new Greeter('Hello')
}
})()

// script file 2
class Greeter {
constructor (greeting) {
this._greeting = greeting
}
hello (person) {
console.log(`${this._greeting} ${person}`)
}
}

// script file 3
const main = () => {
Fr.greeter.hello ('Pierre')
En.greeter.hello ('Peter')
}

// script file 4
const Fr = (() => {
return {
greeter: new Greeter('Bonjour')
}
})()
scripts – this will fail when the iffys are running

When you run ‘main’, you’ll get this

ReferenceError: Greeter is not defined
(anonymous) @ En.gs:3
(anonymous) @ En.gs:7
Defined in the wrong order

Let’s not do that then – but we do want to keep those iffys, as they are handy to keep the global namespace free of accidental noise.

Property Getters

You can solve this by returning property getters, which will postpone the attempt to access anything until they are actually invoked by your main script.

Here’s script 2 and 4, rewritten with property getters.

//script file 1
const En = (() => {
return {
get greeter() {
return new Greeter('Hello')
}
}
})()

//script file 4
const Fr = (() => {
return {
get greeter() {
return new Greeter('Bonjour')
}
}
})()
script 2 and 4 iffys re-written with poperty getters

… and now it works

1:06:51 PM	Notice	Execution started
1:06:51 PM Info Bonjour Pierre
1:06:51 PM Info Hello Peter
1:06:52 PM Notice Execution completed
execution with property getters

Exports

Centralizing all the accessed namespaces and classes gives you flexibility on ‘post-processing’ of these classes and iffys – I’ll give and example in a moment but first let’s create an Exports object in a new script and define each of these scripts in it.

// script 5
var Exports = {
get Greeter () {
return Greeter
},
get En () {
return En
},
get Fr () {
return Fr
}
}

//script file 1
const En = (() => {
return {
get greeter() {
return new Exports.Greeter('Hello')
}
}
})()

// script file 3
const main = () => {
Exports.Fr.greeter.hello ('Pierre')
Exports.En.greeter.hello ('Peter')
}

//script file 4
const Fr = (() => {
return {
get greeter() {
return new Exports.Greeter('Bonjour')
}
}
})()
with exports

Now we are accessing each of the classes and iffys via a central Exports object. This means we can fiddle with their definition without needing to change any other code. Let’s say that the Fr and En iffys now came from libraries (a common requirement when developing or testing libraries). All we’d need to do is modify the Exports object like this, and all references in all your scripts would immediately switch to the new definition.

var Exports = {
get Greeter () {
return Greeter
},
get En () {
return SomeLibrary.En
},
get Fr () {
return SomeOtherLibrary.Fr
}
}
modifying the exports object

Validating property access

Now that we have all the Classes and Namespaces going through the Exports funnel, we can handle one of the bigger JavaScript problems, namely noticing whether you have accessed a non-existent property.

Let’s enhance the scripts a little.

//script file 1

const En = (() => {
return {
get greeter() {
return new Exports.Greeter({
salutation: 'Hello',
language: 'English'
})
}
}
})()


// script file 2
class Greeter {
constructor ({language, salutation}) {
this.language = language
this.salutation = salutation
}
hello (person) {
console.log(`(${this.language}) ${this.salutation} ${person}`)
}
}

// script file 3
const main = () => {
Exports.Fr.greeter.hello ('Pierre')
Exports.En.greeter.hello ('Peter')
}


//script file 4
const Fr = (() => {
return {
get greeter() {
return new Exports.Greeter({
salutation: 'Bonjour',
language: 'Français'
})
}
}
})()
enhanced scripts

Now we get this

2:21:54 PM	Notice	Execution started
2:21:55 PM Info (Français) Bonjour Pierre
2:21:55 PM Info (English) Hello Peter
2:21:55 PM Notice Execution completed
enhanced result

A common problem is misspelling a property name, so this

  console.log(Exports.En.greeter.language)
console.log(Exports.En.greeter.Language)
common misspelling

Gives this

2:24:15 PM	Info	English
2:24:15 PM Info undefined
Misspelling results

That ‘undefined’ is a real problem. We didn’t notice it was an error, so now all kinds of things might go wrong later in the script, and it’ll be hard to track down the root cause.

Proxies

This can be fixed by using a JavaScript Proxy, which gives us the opportunity intercept all calls to an object and check them or even fiddle with them. So let’s create a guard function in the Exports object.

  // used to trap access to unknown properties
guard(target) {
return new Proxy(target, this.validateProperties)
},

/**
* for validating attempts to access non existent properties
*/
get validateProperties() {
return {
get(target, prop, receiver) {
if (!Reflect.has(target, prop)) throw `guard detected attempt to get non-existent property ${prop}`
return Reflect.get(target, prop, receiver)
},

set(target, prop, value, receiver) {
if (!Reflect.has(target, prop)) throw `guard attempt to set non-existent property ${prop}`
return Reflect.set(target, prop, value, receiver)
}
}
}
guard function

and use that guard function to police access to the objects we are Exporting. First though we’ll need to add an instation function in place of ‘new’ when creating a class. The final 5 scripts now look this this.

//script file 1
const En = (() => {
return {
get greeter() {
return Exports.newGreeter({
salutation: 'Hello',
language: 'English'
})
}
}
})()

// script file 2
class Greeter {
constructor ({language, salutation}) {
this.language = language
this.salutation = salutation
}
hello (person) {
console.log(`(${this.language}) ${this.salutation} ${person}`)
}
}

// script file 3
const main = () => {
Exports.Fr.greeter.hello ('Pierre')
Exports.En.greeter.hello ('Peter')
}

//script file 4
const Fr = (() => {
return {
get greeter() {
return Exports.newGreeter({
salutation: 'Bonjour',
language: 'Français'
})
}
}
})()

var Exports = {
get Greeter() {
return Greeter
},

newGreeter (...args) {
return this.guard ( new this.Greeter(...args))
},

get En() {
return this.guard(En)
},
get Fr() {
return this.guard(Fr)
},

// used to trap access to unknown properties
guard(target) {
return new Proxy(target, this.validateProperties)
},

/**
* for validating attempts to access non existent properties
*/
get validateProperties() {
return {
get(target, prop, receiver) {
if (!Reflect.has(target, prop)) throw `guard detected attempt to get non-existent property ${prop}`
return Reflect.get(target, prop, receiver)
},

set(target, prop, value, receiver) {
if (!Reflect.has(target, prop)) throw `guard attempt to set non-existent property ${prop}`
return Reflect.set(target, prop, value, receiver)
}
}
}
}
Guarded properties

Let’s see what happens when we try to access a non existent property again.

2:40:24 PM	Info	English
2:40:25 PM Error
guard detected attempt to get non-existent property Language
get @ Exports.gs:28
main @ Main.gs:6
Guarded results

Perfect – now detect and say goodbye to those missed typos whenever you make them!

Checking function arguments

In the tutorials I’m working on at the moment, there’s sometimes a need for the learner to supply a function, for example to compare values, where the values are properties plucked from an object. Here’s a simplified example in the context of our scripts so far.

// script file 3
const main = () => {

const obSorter = (prop, arr) =>
arr.sort ((a,b)=> a[prop] === b[prop] ? 0 : (a[prop] > b[prop] ? 1 : -1))

const list = obSorter('name', [{
name: 'Pierre',
ns: 'Fr'
}, {
name: 'Peter',
ns: 'En'
}, {
name: 'Billy',
ns: 'En'
}])


list.forEach (item=>Exports[item.ns].greeter.hello(item.name))
}
function accessing properties of its arguments

Which gives this result

3:00:47 PM	Notice	Execution started
3:00:47 PM Info (English) Hello Billy
3:00:47 PM Info (English) Hello Peter
3:00:47 PM Info (Français) Bonjour Pierre
3:00:47 PM Notice Execution completed
obsorter result

But if the property name is wrong, it will silently appear to do the sort, but give the wrong answer. Much better if we can have a reusable way to check the validity of the property argument. Yes – we can use the guard function again.

Here’s a guarded version

// script file 3
const main = () => {

const obSorter = (prop, arr) =>
arr.sort((a, b) => {
const guarda = Exports.guard(a)
const guardb = Exports.guard(b)
return guarda[prop] === guardb[prop] ? 0 : (guarda[prop] > guardb[prop] ? 1 : -1)
})


const list = obSorter('name', [{
name: 'Pierre',
ns: 'Fr'
}, {
name: 'Peter',
ns: 'En'
}, {
name: 'Billy',
ns: 'En'
}])


list.forEach(item => Exports[item.ns].greeter.hello(item.name))
}
guarded function arguments

This works as before, but this time if we accidentally pass a typo as the property name, this happens.

3:07:50 PM	Notice	Execution started
3:07:51 PM Error
guard detected attempt to get non-existent property Name
get @ Exports.gs:28
(anonymous) @ Main.gs:8
obSorter @ Main.gs:5
main @ Main.gs:12
typo in argument name

Summary

These approaches have saved me endless debugging time, especially with the added complication of testing incremental code in tutorials. I highly recommend you give it a go.

Related

Use a proxy to catch invalid property access

JavaScript proxy, fiddling with Apps Script objects and custom errors