Skip to content
Skip to chapter

Chapter 6

JavaScript and the runtime

Understand the runtime and permissions

Compare two Apps Script projects to see where code runs, what a bound project adds, and how the manifest, OAuth scopes, and running account limit access.

By the end

Explain why a project can run JavaScript calculations yet lack a parent document or a permission, and change one harmless manifest setting without broadening access.

Chapter navigation

Explain what a run is allowed to do

A script's access depends on where the code runs, whether the project has a parent document, which services it uses, the manifest settings, the OAuth scopes the user approved, and which account runs it.

You need the two projects from earlier chapters:

Both run JavaScript on the V8 runtime. Compare the two projects against each factor, then change and restore the timeZone field in Customer arrays.

The editor runs in a browser, and the script runs on Google's servers

A runtime is the environment that executes code and provides its global objects. The browser tab shows the Apps Script editor, but Apps Script's V8 runtime executes .gs files on Google's servers.

Standard JavaScript built-ins such as Array, String, and Math come from the V8 engine. Apps Script provides host globals such as Logger and SpreadsheetApp. See Google's built-in-services guide.

Different JavaScript environments expose different host APIs:

Code environmentObjects it can useWhat that means here
Apps Script server code in .gs filesJavaScript built-ins and Apps Script servicessummarizeActiveCustomers can use array methods and Logger.log.
JavaScript inside a browser pageBrowser objects such as window and documentBrowser globals do not exist in .gs server files.
Node.js on a computer or serverNode APIs such as process and file-system modulesNode built-in modules cannot be imported into Code.gs.

Apps Script server code has no browser DOM (window, document), no browser fetch, no Node require, and no ES module import or export. To make HTTP requests from .gs code, use UrlFetchApp. See Google's V8 runtime limitations.

V8 is the standard Apps Script runtime. Older manifests may specify "runtimeVersion": "STABLE" or "DEPRECATED_ES5", which refer to the legacy Rhino runtime (manifest reference). Google deprecated Rhino on January 31, 2026. If you inherit a Rhino script, follow the V8 migration guide and test it on a copy. Every example in this book uses "runtimeVersion": "V8".

When an Apps Script project later serves an HTML dialog, sidebar, or web app, the browser JavaScript inside that HTML runs in the user's browser while .gs files run on Google's servers. Neither environment shares the other's global objects.

A bound project has a parent document

A bound project is attached to one spreadsheet, document, presentation, or form, and you open it from that file with Extensions > Apps Script. A standalone project, such as Customer arrays, is its own file in Drive and has no parent document. Both project types can call Apps Script services. See the bound-project guide and standalone-project guide.

The project type determines how code locates a file. In a bound spreadsheet script run from the editor, SpreadsheetApp.getActiveSpreadsheet() returns the parent spreadsheet. In a standalone project, getActiveSpreadsheet() returns null even if you have a spreadsheet open in another browser tab.

Compare the read-only Sheets script from Run your first Sheets script:

Code.gs
function readAllCustomerRows() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Customers');
  if (!sheet) throw new Error('Add a Sheet tab named Customers before running this function.');
  const values = sheet.getDataRange().getValues();
  Logger.log(values);
}

Download Code.gs

Its first line chains SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Customers'). If getActiveSpreadsheet() returns null in a standalone project, calling getSheetByName throws a TypeError before the missing-tab check runs. To open a spreadsheet from a standalone project, pass a file ID to SpreadsheetApp.openById() instead.

Even in a bound project, active-document and UI methods depend on how the script starts. Google's bound-script guide notes that getActiveSpreadsheet() and UI methods are unavailable when the project runs as a web app or through the Apps Script API.

Choose a service for the required operation

SpreadsheetApp is the built-in service for Google Sheets. Built-in services are globals. You do not import them or add them under Services. On a Range object, getValues() reads cell values into a JavaScript array, and setValues() overwrites cells in the sheet.

An advanced Google service provides Apps Script methods for a public Google REST API. For example, the advanced Sheets service wraps the Google Sheets API v4, while SpreadsheetApp is the built-in service for the same product. When a task requires an advanced service, enable it under Services in the editor. Enabling it records its identifier, service ID, and version under dependencies.enabledAdvancedServices in appsscript.json.

Apps Script enables the underlying Cloud API automatically for default Google Cloud projects. For standard Cloud projects, enable the API in the Cloud console yourself (advanced-services guide).

Read configuration separately from code

Every Apps Script project has an appsscript.json manifest that stores project settings (manifest guide). Show it in Project Settings by checking Show "appsscript.json" manifest file in editor. appsscript.json is strict JSON. Keys and strings need double quotes, and a comment or trailing comma stops the file from saving.

For comparison, this is the Customer arrays code:

Code.gs
function summarizeActiveCustomers() {
  const customerRows = [
    ['Name', 'Email', 'Status'],
    ['Avery Quinn', 'avery@example.com', 'Active'],
    ['Morgan Lee', 'morgan@example.com', 'Paused'],
    ['Riley Park', 'riley@example.com', 'Active']
  ];

  const headers = customerRows[0];
  const nameIndex = headers.indexOf('Name');
  const emailIndex = headers.indexOf('Email');
  const statusIndex = headers.indexOf('Status');
  if (statusIndex === -1) {
    throw new Error('Add a Status header before running this function.');
  }
  if (nameIndex === -1) {
    throw new Error('Add a Name header before running this function.');
  }
  if (emailIndex === -1) {
    throw new Error('Add an Email header before running this function.');
  }

  const dataRows = customerRows.slice(1);
  const customers = dataRows.map((row) => {
    return {
      Name: row[nameIndex],
      Email: row[emailIndex],
      Status: row[statusIndex]
    };
  });
  const activeCustomers = customers.filter(isActiveCustomer);
  const activeNames = activeCustomers.map((customer) => String(customer.Name));

  Logger.log('Active customers: ' + activeNames.length);
  Logger.log(activeNames.join(', '));
}

/**
 * @param {{Name: string, Email: string, Status: string}} customer
 * @returns {boolean}
 */
function isActiveCustomer(customer) {
  return customer.Status === 'Active';
}

Download Code.gs

These are the two manifests, first the Sheets project and then Customer arrays:

appsscript.json
{
  "timeZone": "Etc/UTC",
  "dependencies": {},
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "oauthScopes": [
    "https://www.googleapis.com/auth/spreadsheets.currentonly"
  ]
}

Download appsscript.json
appsscript.json
{
  "timeZone": "Etc/UTC",
  "dependencies": {},
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8"
}

Download appsscript.json

The fields mean the following (manifest structure reference):

FieldMeaning in these projects
runtimeVersion: "V8"Selects the V8 JavaScript runtime.
timeZone: "Etc/UTC"Sets the project time zone used by Session.getScriptTimeZone() and time-driven triggers.
dependencies: {}Declares enabled advanced services and libraries. Built-in services do not appear here.
exceptionLogging: "STACKDRIVER"Sends uncaught exception traces to Cloud Logging.
oauthScopesPresent in the Sheets project. Lists the exact OAuth scopes the project requests.

An OAuth scope identifies a category of Google service access. When oauthScopes is omitted from appsscript.json, Apps Script infers scopes from the service calls it finds. It scans all code files in the project, not only the function selected in the toolbar.

When oauthScopes is present in appsscript.json, Apps Script requests only that list. Update oauthScopes whenever you add service calls that require new permissions (scope guide).

The first Sheets project declares https://www.googleapis.com/auth/spreadsheets.currentonly. That scope limits access to the bound spreadsheet, so the script cannot open other spreadsheets in Drive. It is not read-only: both getValues() and setValues() use spreadsheets.currentonly. The first script is read-only because its code calls getValues() and never calls a write method.

Consent is the user's approval of the requested scopes on the OAuth prompt (authorization guide). OAuth consent does not override Google Drive file permissions. For a service call to succeed, the executing user must have view or edit access to the target file.

Google's consent screen can list each requested scope with its own checkbox, so a user can approve some scopes and leave others unchecked (granular OAuth consent). Your script receives only the approved scopes. A later call that needs an unapproved scope throws an authorization error in the middle of the run.

ScriptApp provides two methods that check consent before your function reads or writes data:

  • ScriptApp.requireScopes(authMode, scopes) checks the listed scopes and halts execution with an authorization prompt if any are missing.

  • ScriptApp.requireAllScopes(authMode) checks every scope declared or inferred for the project.

Call the check at the start of an entrypoint before reading or writing data:

Source
function updateCustomerStatuses() {
  ScriptApp.requireScopes(ScriptApp.AuthMode.FULL, [
    'https://www.googleapis.com/auth/spreadsheets.currentonly',
  ]);
  // The spreadsheet work starts here, after consent is confirmed.
}

To inspect consent without halting immediately, call ScriptApp.getAuthorizationInfo(authMode, scopes). On the object it returns, getAuthorizationStatus() reports whether additional consent is needed, getAuthorizedScopes() returns the approved scope URLs, and getAuthorizationUrl() returns a URL the user can open to grant missing scopes (ScriptApp reference).

An installable trigger runs in the background with no user at the keyboard, so it cannot show a consent prompt and fails at once if a required scope is missing. Call requireScopes inside the function that installs the trigger, so the user grants consent during setup.

Name the user behind the execution

The execution identity is the account whose permissions a run uses. When you click Run in the editor for a bound or standalone script, the code executes as your signed-in account.

The execution identity is not the last person who edited Code.gs. An installable trigger always runs as the user who created the trigger, and a web app runs as either the visitor or the script owner, depending on its deployment settings (authorization identity table).

Inspect, change and restore one manifest field

Use the standalone Customer arrays project to test a manifest change:

  1. In Customer arrays, verify that summarizeActiveCustomers is selected and appsscript.json has "runtimeVersion": "V8", "timeZone": "Etc/UTC", "dependencies": {}, and no oauthScopes field.

  2. Run summarizeActiveCustomers and confirm the log shows Active customers: 2 and Avery Quinn, Riley Park without prompting for OAuth consent.

  3. In appsscript.json, change "timeZone" from "Etc/UTC" to "America/New_York" and save.

  4. Run summarizeActiveCustomers again. The two logged lines are identical, because the function compares strings in memory and never reads the project time zone.

  5. Restore "timeZone" to "Etc/UTC", save, and rerun to confirm the original output.

Find the cause of an error

Match each error to its cause:

  • Manifest will not save: Check appsscript.json for missing double quotes or trailing commas.

  • TypeError: Cannot read properties of null (reading 'getSheetByName'): getActiveSpreadsheet() returned null because the code ran outside a bound spreadsheet context. Open the bound project from the spreadsheet with Extensions > Apps Script (see Cannot read properties of null).

  • TypeError: X is not a function: The method does not exist on that object, such as calling a Sheet method on a Range or Spreadsheet. Check the receiver's class in the reference (see TypeError: X is not a function).

  • ReferenceError: window is not defined (or document, require, fetch): The code uses a browser or Node global that does not exist on Google's servers.

  • Authorization or permission error: Check the project's oauthScopes, the executing user's granted consent, and that user's Drive sharing permissions on the target file (authorization troubleshooting).

Continue to Read and write Sheets in batches to read a table once, compute updates in JavaScript arrays, and write the updated column back in a single call.

Project files

Complete source files for this chapter's examples.

Read all customer rows
Code.gs
function readAllCustomerRows() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Customers');
  if (!sheet) throw new Error('Add a Sheet tab named Customers before running this function.');
  const values = sheet.getDataRange().getValues();
  Logger.log(values);
}

appsscript.json

Download file
appsscript.json
{
  "timeZone": "Etc/UTC",
  "dependencies": {},
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "oauthScopes": [
    "https://www.googleapis.com/auth/spreadsheets.currentonly"
  ]
}

sample.csv

Download file
sample.csv
Name,Email,Status
Avery Quinn,avery@example.com,Active
Morgan Lee,morgan@example.com,Paused
Riley Park,riley@example.com,Active

Filter active customers in a two-dimensional array
Code.gs
function summarizeActiveCustomers() {
  const customerRows = [
    ['Name', 'Email', 'Status'],
    ['Avery Quinn', 'avery@example.com', 'Active'],
    ['Morgan Lee', 'morgan@example.com', 'Paused'],
    ['Riley Park', 'riley@example.com', 'Active']
  ];

  const headers = customerRows[0];
  const nameIndex = headers.indexOf('Name');
  const emailIndex = headers.indexOf('Email');
  const statusIndex = headers.indexOf('Status');
  if (statusIndex === -1) {
    throw new Error('Add a Status header before running this function.');
  }
  if (nameIndex === -1) {
    throw new Error('Add a Name header before running this function.');
  }
  if (emailIndex === -1) {
    throw new Error('Add an Email header before running this function.');
  }

  const dataRows = customerRows.slice(1);
  const customers = dataRows.map((row) => {
    return {
      Name: row[nameIndex],
      Email: row[emailIndex],
      Status: row[statusIndex]
    };
  });
  const activeCustomers = customers.filter(isActiveCustomer);
  const activeNames = activeCustomers.map((customer) => String(customer.Name));

  Logger.log('Active customers: ' + activeNames.length);
  Logger.log(activeNames.join(', '));
}

/**
 * @param {{Name: string, Email: string, Status: string}} customer
 * @returns {boolean}
 */
function isActiveCustomer(customer) {
  return customer.Status === 'Active';
}

appsscript.json

Download file
appsscript.json
{
  "timeZone": "Etc/UTC",
  "dependencies": {},
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8"
}