Migrating scripts to the V8 runtime

If you have an existing script using the Rhino runtime and want to make use of V8 syntax and features, you must migrate the script to V8.

Most scripts written using the Rhino runtime can operate using a V8 runtime without adjustment. Often the only prerequisite to adding V8 syntax and features to a script is enabling the V8 runtime.

However, there is a small set of incompatibilities and other differences that can result in a script failing or behaving unexpectedly after enabling the V8 runtime. As you migrate a script to use V8, you must search the script project for these issues and correct any you find.

V8 migration procedure

To migrate a script to V8, follow this procedure:

  1. Enable the V8 runtime for the script.
  2. Carefully review the incompatibilites listed below. Examine your script to determine if any of the incompatibilities are present; if one or more incompatibilities are present, adjust your script code to remove or avoid the issue.
  3. Carefully review the other differences listed below. Examine your script to determine if any of the listed differences impact your code's behavior. Adjust your script to correct the behavior.
  4. Once you have corrected any discovered incompatibilities or other differences, you can begin updating your code to use V8 syntax and other features as desired.
  5. After finishing your code adjustments, thoroughly test your script to make sure it behaves as expected.
  6. If your script is a web app or published add-on, you must create a new version of the script with the V8 adjustments. To make the V8 version available to users, you must re-publish the script with this version.

Incompatibilities

The original Rhino-based Apps Script runtime unfortunately permitted several non-standard ECMAScript behaviors. Since V8 is standards compliant, these behaviors aren't supported after migration. Failing to correct these issues results in errors or broken script behavior once the V8 runtime is enabled.

The following sections describe each of these behaviors and steps you must take to correct your script code during migration to V8.

Avoid for each(variable in object)

The for each (variable in object) statement was added to JavaScript 1.6, and removed in favor of for...of.

When migrating your script to V8, avoid using for each (variable in object) statements.

Instead, use for (variable in object):

// Rhino runtime
var obj = {a: 1, b: 2, c: 3};

// Don't use 'for each' in V8
for each (var value in obj) {
  Logger.log("value = %s", value);
}
      
// V8 runtime
var obj = {a: 1, b: 2, c: 3};

for (var key in obj) {  // OK in V8
  var value = obj[key];
  Logger.log("value = %s", value);
}
      

Avoid Date.prototype.getYear()

In the original Rhino runtime, Date.prototype.getYear() returns two-digit years for years from 1900-1999, but four-digit years for other dates, which was the behavior in JavaScript 1.2 and earlier.

In the V8 runtime, Date.prototype.getYear() returns the year minus 1900 instead as required by ECMAScript standards.

When migrating your script to V8, always use Date.prototype.getFullYear(), which returns a four-digit year regardless of the date.

Avoid using reserved keywords as names

ECMAScript prohibits the use of certain reserved keywords in function and variable names. The Rhino runtime allowed many of these words, so if your code uses them, you must rename your functions or variables.

When migrating your script to V8, avoid naming variable or functions using one of the reserved keywords. Rename any variable or function to avoid using the keyword name. Common uses of keywords as names are class, import, and export.

Avoid reassigning const variables

In the original Rhino runtime, you can declare a variable using const which means the value of the symbol never changes and future assignments to the symbol are ignored.

In the new V8 runtime, the const keyword is standard compliant and assigning to a variable declared as a const results in a TypeError: Assignment to constant variable runtime error.

When migrating your script to V8, do not attempt to reassign the value of a const variable:

// Rhino runtime
const x = 1;
x = 2;          // No error
console.log(x); // Outputs 1
      
// V8 runtime
const x = 1;
x = 2;          // Throws TypeError
console.log(x); // Never executed
      

Avoid XML literals and the XML object

This non-standard extension to ECMAScript allows Apps Script projects to use XML syntax directly.

When migrating your script to V8, avoid using direct XML literals or the XML object.

Instead, use the XmlService to parse XML:

// V8 runtime
var incompatibleXml1 = <container><item/></container>;             // Don't use
var incompatibleXml2 = new XML('<container><item/></container>');  // Don't use

var xml3 = XmlService.parse('<container><item/></container>');     // OK
      

Don't build custom iterator functions using __iterator__

JavaScript 1.7 added a feature to allow adding a custom iterator to any clas s by declaring a __iterator__ function in that class's prototype; this was also added into Apps Script's Rhino runtime as a developer convenience. However, this feature was never part of the ECMA-262 standard and was removed in ECMAScript-compliant JavaScript engines. Scripts using V8 can't use this iterator construction.

When migrating your script to V8, avoid __iterator__ function to build custom iterators. Instead, use ECMAScript 6 iterators.

Consider the following array construction:

// Create a sample array
var myArray = ['a', 'b', 'c'];
// Add a property to the array
myArray.foo = 'bar';

// The default behavior for an array is to return keys of all properties,
//  including 'foo'.
Logger.log("Normal for...in loop:");
for (var item in myArray) {
  Logger.log(item);            // Logs 0, 1, 2, foo
}

// To only log the array values with `for..in`, a custom iterator can be used.
      

The following code examples show how an iterator could be constructed in the Rhino runtime, and how to construct a replacement iterator in the V8 runtime:

// Rhino runtime custom iterator
function ArrayIterator(array) {
  this.array = array;
  this.currentIndex = 0;
}

ArrayIterator.prototype.next = function() {
  if (this.currentIndex
      >= this.array.length) {
    throw StopIteration;
  }
  return "[" + this.currentIndex
    + "]=" + this.array[this.currentIndex++];
};

// Direct myArray to use the custom iterator
myArray.__iterator__ = function() {
  return new ArrayIterator(this);
}


Logger.log("With custom Rhino iterator:");
for (var item in myArray) {
  // Logs [0]=a, [1]=b, [2]=c
  Logger.log(item);
}
      
// V8 runtime (ECMAScript 6) custom iterator
myArray[Symbol.iterator] = function() {
  var currentIndex = 0;
  var array = this;

  return {
    next: function() {
      if (currentIndex < array.length) {
        return {
          value: "[${currentIndex}]="
            + array[currentIndex++],
          done: false};
      } else {
        return {done: true};
      }
    }
  };
}

Logger.log("With V8 custom iterator:");
// Must use for...of since
//   for...in doesn't expect an iterable.
for (var item of myArray) {
  // Logs [0]=a, [1]=b, [2]=c
  Logger.log(item);
}
      

Avoid calling functions before they are parsed

In the original Rhino runtime, the order of files in the Apps Script editor doesn't matter. This meant you could freely call a function in a different file to assign a value to a global variable—the function is always defined before it is called.

In the V8 runtime, however, a script file must be parsed before any other file can call the functions it defines. Files are parsed in the same order as they appear in the Apps Script editor (that is, when View > Sort files alphabetically is disabled). A file's functions may be unavailable when global variable assisngments are made in a different file. This arrangement is identical to how browsers handle multiple <script> tags in one HTML file.

When migrating your script to V8, avoid calling functions in separate files until they are parsed. In this example, an error occurs if the First.gs script file is parsed before the Second.gs script file is parsed:

First.gs

// This fails in V8 if Second.gs isn't
// parsed by the time First.gs is
// being parsed.
var globalVar = calculate();

function myFunction() {
  Logger.log("globalVar = %s", globalVar);
}

Second.gs

// A utility function saved in a separate
// file (Second.gs) in the same script
// project.
function calculate() {
  return Math.random();
}

      

In many cases you can fix this issue by removing global variable assignments that depend on functions in different files. In this example the global variable is moved and left initially unassigned, avoiding the error while maintaining the same functionality:

First.gs

// This works in V8 since calculate()
// does not execute until myFunction2()
// is called.

function myFunction2() {
  var localVar = calculate();
  Logger.log("localVar = %s", localVar);
}


      

Second.gs

// This global variable declaration is OK
// in V8 since there is no function
// call assignment.
var cachedValue;

function calculate() {
  if (cachedValue == undefined) {
    cachedValue = Math.random();
  }
  return cachedValue;
}

Avoid conditional catch clauses

The V8 runtime doesn't support catch..if conditional catch clauses, as they are not standard-compliant.

When migrating your script to V8, move any catch conditionals inside the catch body:

// Rhino runtime

try {
  doSomething();
} catch (e if e instanceof TypeError) {  // Don't use
  // Handle exception
}
      
// V8 runtime
try {
  doSomething();
} catch (e) {
  if (e instanceof TypeError) {
    // Handle exception
  }
}

Avoid using Object.prototype.toSource()

JavaScript 1.3 contained a Object.prototype.toSource() method that was never part of any ECMAScript standard. It is not supported in the V8 runtime.

When migrating your script to V8, remove any use of Object.prototype.toSource() from your code.

Other differences

In addition to the above incompatibilities that can cause script failures, there are a few other differences that, if uncorrected, may result in unexpected V8 runtime script behavior.

The following sections explain how to update your script code to avoid these unexpected surprises.

Adjust locale-specific date and time formatting

The Date methods toLocaleString(), toLocaleDateString(), and toLocaleTimeString() behave differently in the V8 runtime as compared to Rhino.

In Rhino, the default format is the long format, and any parameters passed in are ignored.

In the V8 runtime, the default format is the short format and parameters passed in are handled according to the ECMA standard (see the toLocaleDateString() documentation for details).

When migrating your script to V8, test and adjust your code's expectations regarding the output of locale-specific date and time methods:

// Rhino runtime
var event = new Date(
  Date.UTC(2012, 11, 21, 12));

// Outputs "December 21, 2012" in Rhino
console.log(event.toLocaleDateString());

// Also outputs "December 21, 2012",
//  ignoring the parameters passed in.
console.log(event.toLocaleDateString(
    'de-DE',
    { year: 'numeric',
      month: 'long',
      day: 'numeric' }));
// V8 runtime
var event = new Date(
  Date.UTC(2012, 11, 21, 12));

// Outputs "12/21/2012" in V8
console.log(event.toLocaleDateString());

// Outputs "21. Dezember 2012"
console.log(event.toLocaleDateString(
    'de-DE',
    { year: 'numeric',
      month: 'long',
      day: 'numeric' }));
      

Avoid using Error.fileName and Error.lineNumber

In the V8 untime, the standard JavaScript Error object doesn't support the fileName or lineNumber as constructor parameters or object properties.

When migrating your script to V8, remove any dependence on Error.fileName and Error.lineNumber.

An alternative is to use the Error.prototype.stack. This stack is also non-standard, but supported in both Rhino and V8. The format of the stack trace produced by the two platforms is slightly different:

// Rhino runtime Error.prototype.stack
// stack trace format
at filename:92 (innerFunction)
at filename:97 (outerFunction)


// V8 runtime Error.prototype.stack
// stack trace format
Error: error message
at innerFunction (filename:92:11)
at outerFunction (filename:97:5)
      

Adjust handling of stringified enum objects

In the original Rhino runtime, using the JavaScript JSON.stringify() method on an enum object only returns {}.

In V8, using the same method on an enum object retuns the enum name.

When migrating your script to V8, test and adjust your code's expectations regarding the output of JSON.stringify() on enum objects:

// Rhino runtime
var enumName =
  JSON.stringify(Charts.ChartType.BUBBLE);

// enumName evaluates to {}
// V8 runtime
var enumName =
  JSON.stringify(Charts.ChartType.BUBBLE);

// enumName evaluates to "BUBBLE"

Adjust handling of undefined paramaters

In the original Rhino runtime, passing undefined to a method as a parameter resulted in passing the string "undefined" to that method.

In V8, passing undefined to methods is equivalent to passing null.

When migrating your script to V8, test and adjust your code's expectations regarding undefined parameters:

// Rhino runtime
SpreadsheetApp.getActiveRange()
    .setValue(undefined);

// The active range now has the string
// "undefined"  as its value.
      
// V8 runtime
SpreadsheetApp.getActiveRange()
    .setValue(undefined);

// The active range now has no content, as
// setValue(null) removes content from
// ranges.

Adjust handling of global this

The Rhino runtime defines an implicit special context for scripts that use it. Script code runs in this implicit context, distinct from the actual global this. This means that references to the "global this" in the code actually evaluate to the special context, which only contains the code and variables defined in the script. The built-in Apps Script services and ECMAScript object s are excluded from this use of this. This situation was similar to this JavaScript structure:

// Rhino runtime

// Apps Script built-in services defined here, in the actual global context.
var SpreadsheetApp = {
  openById: function() { ... }
  getActive: function() { ... }
  // etc.
};

function() {
  // Implicit special context; all your code goes here. If the global this
  // is referenced in your code, it only contains elements from this context.

  // Any global variables you defined.
  var x = 42;

  // Your script functions.
  function myFunction() {
    ...
  }
  // End of your code.
}();

In V8, the implicit special context is removed. Global variables and functions defined in the script are placed in the global context, beside the built-in Apps Script services and ECMAScript built-ins like Math and Date.

When migrating your script to V8, test and adjust your code's expectations regarding the use of this in a global context. In most cases the differences are only apparent if your code examines the keys or property names of the global this object:

// Rhino runtime
var myGlobal = 5;

function myFunction() {

  // Only logs [myFunction, myGlobal];
  console.log(Object.keys(this));

  // Only logs [myFunction, myGlobal];
  console.log(
    Object.getOwnPropertyNames(this));
}





      
// V8 runtime
var myGlobal = 5;

function myFunction() {

  // Logs an array that includes the names
  // of Apps Script services
  // (CalendarApp, GmailApp, etc.) in
  // addition to myFunction and myGlobal.
  console.log(Object.keys(this));

  // Logs an array that includes the same
  // values as above, and also includes
  // ECMAScript built-ins like Math, Date,
  // and Object.
  console.log(
    Object.getOwnPropertyNames(this));
}