Apps Script const scoping problems

Apps Script (intermediate level) posted on 25th October 2018


Apps Script is based on ES3 JavaScript, with quite a few additions from ES5 and even ES6. In this article we'll take a look at the implementation of const,  and let which were introduced in ES6.

The difference between var, let and const.


Prior to ES6, a variable was declared using var, as in 
var x = 20;

However, var is dogged by what's known as "hoisting". Consider this
function f() {
    Logger.log (x);
}

Quite rightly, this fails with this error message.

But it's quite happy with this 
function f() {
    Logger.log (x);
     var x = 20;
}

So even though a variable isn't yet defined, it doesn't flag an error. This is not just an Apps Script thing, but inherent in JavaScript. Its why you can call a function before you define it. Referenced variables are hoisted to the beginning of their scope. This kind of thing can cause obscure bugs, so was one of the reasons that const and let were introduced. 

const

The purpose of const is to define a value you set only once. In regular JavaScript, if you assign a value to a const more than once in the same block, you'll get an error.
And in Apps Script, you get a similar message
function f() {
    const x = 20;
    const x = 21;
}

An in execution,  JavaScript also detects an error when you do this

But Apps Script doesn't complain, even though this is plain wrong.
function f() {
    const x = 20;
    x = 21;
    Logger.log(x);
}

And what's more, the value is the initial one  (20), not the last assigned one (21) - which makes the use of const in Apps Script even more error prone than var.

let

Let is to define a block scoped variable whose value is going to change. Here it is in JavaScript, and the final value is 21
(function f() {
     let x = 20; 
     x = 21;
     console.log (x);
})();

let doesn't exist in Apps Script so that takes us back to the drawing board, using var. 

Block scopes

The main purpose of const and let though is prevent the leakage that occurs when you use var. 

This works just fine
for (var i=0; i < 2 ; i++) {
    Logger.log (i);
}

But imagine you copy this bit of code into another function
function f() {
    var i =20;
    if (i === 20) {
       Logger.log ("all is good");
    }
    else {
       Logger.log ("all is bad");
    }
}

to make this
function f() {
    var i =20;
    for (var i=0; i < 2 ; i++) {
      Logger.log (i);
    }
    if (i === 20) {
       Logger.log ("all is good");
    }
    else {
       Logger.log ("all is bad");
    }
}
You just broke your function. 

The purpose of let then is to define a variable for the lifetime of the block it's in. Since it doesn't exist in Apps Script, we have to go to JavaScript for this.
(function f() {
    let i =20;
    for (let i=0; i < 2 ; i++) {
      Logger.log (i);
    }
    if (i === 20) {
       Logger.log ("all is good");
    }
    else {
       Logger.log ("all is bad");
    }
})();
 0
 1
 all is good

So what's going on here is that the 'i' in the for loop is a different 'i' than the 'i' outside it. This is because of the block scoping of 'let' as opposed to the function scoping of 'var'

const inside a block

Although const is about not reassigning a value to the same variable within a block, each iteration of a block is a new day, so it's perfectly fine to do this. In fact it is emphasising that 'a' should only take an initial value for each loop iteration, whereas 'b' is supposed to be changed.

(function f() {
    
    for (let i=0; i < 2 ; i++) {
      const a = i +1;
      let b = a;
      Logger.log (a);
      b = b+1;
    }

})();
 1
 2

However, Apps Script doesn't do let, and neither does it deal with const properly, so we'll have to use var.
function f() {
    
    for (var i=0; i < 2 ; i++) {
      const a = i +1;
      var b = a;
      Logger.log (a);
      b = b+1;
    }
    Logger.log (a);
}
 1
 1
 1
The result is wrong in multiple ways
  • 'a' takes the first value assigned to it (1) and doesn't get changed by subsequent iterations, yet neither does it complain about 'a' being assigned to a constant.
  • 'a' leaks outside its block. 'a' should be undefined outside the for loop

const in global space to assign to a function

Consider this - it works fine
function f() {
  const t = function () {
    return 't';
  }
  Logger.log (t());
}

Now let's hoist the function declaration into the global namespace.
const t = function () {
    return 't';
}
function f() {
  Logger.log (t());
}


Conclusion 

Avoid using const in Apps Script for now. It just doesn't work as it should.  For now it's just a half baked version of var.

It looks like Apps Script is using the Rhino JavaScript engine, and judging by what works and what doesn't, I'd go for v1.7r3. I've done an analysis on supported capabilities here - What JavaScript engine is Apps Script running on?

If anyone wants to check, here's a list of JS supported capabilities by rhino version. http://mozilla.github.io/rhino/compat/engines.html

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, All formats are available from O'ReillyAmazon 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.

Comments