Skip to content
Skip to chapter

Chapter 4

JavaScript and the runtime

Make decisions with functions and loops

Write reusable JavaScript functions in Google Apps Script using parameters, return values, conditional branches, and a bounded iteration loop.

By the end

Call helpers with explicit inputs, follow their returned decisions, and explain which variables each function can use and why its loop stops.

Chapter navigation

Turn a request into a decision

Decide in memory whether a training session fits in a room. Building on Work with JavaScript values and objects, this chapter splits the calculation into helper functions so the entrypoint requests a decision rather than performing every step inline.

The room has three tables, each seating two people. The request asks for five seats with approved: true. The script checks approval and the seat count, tries one, two, and three tables until it finds enough capacity, and logs the decision.

A function declaration names a block of instructions, and a call executes them. This project defines an editor entrypoint, planTrainingRoom, and two helper functions: decideRoomRequest chooses the decision string, and findTableCount calculates how many tables are needed.

Install the standalone project

Create a standalone project that uses JavaScript values and Logger.

Code.gs
const MAX_TABLES = 3;

/**
 * @typedef {{title: string, approved: boolean, seats: number}} RoomRequest_
 */

function planTrainingRoom() {
  const request = {
    title: 'Training session',
    approved: true,
    seats: 5,
  };
  const seatsPerTable = 2;
  const decision = decideRoomRequest(request, seatsPerTable);

  Logger.log('Request: ' + request.title);
  Logger.log('Decision: ' + decision);
  Logger.log('Decision again: ' + decideRoomRequest(request, seatsPerTable));
  Logger.log('Seat count after planning: ' + request.seats);
}

/**
 * @param {RoomRequest_} roomRequest
 * @param {number} seatsPerTable
 * @returns {string}
 */
function decideRoomRequest(roomRequest, seatsPerTable) {
  if (roomRequest.approved !== true) {
    return 'Needs approval';
  }
  if (Number.isInteger(roomRequest.seats) !== true) {
    return 'Use a whole-number seat count';
  }
  if (roomRequest.seats < 1) {
    return 'Request at least one seat';
  }

  const tableCount = findTableCount(roomRequest.seats, seatsPerTable);
  if (tableCount === null) {
    const capacity = MAX_TABLES * seatsPerTable;
    return 'Too large; room capacity: ' + capacity + ' seats';
  } else {
    return 'Ready; tables needed: ' + tableCount;
  }
}

/**
 * @param {number} seatCount
 * @param {number} seatsPerTable
 * @returns {number | null}
 */
function findTableCount(seatCount, seatsPerTable) {
  let tableCount = 0;
  while (tableCount < MAX_TABLES) {
    tableCount = tableCount + 1;
    const capacity = tableCount * seatsPerTable;
    if (capacity >= seatCount) {
      return tableCount;
    }
  }
  return null;
}

Download Code.gs

The project manifest sets the V8 runtime and UTC time zone:

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

Download appsscript.json
  1. At script.google.com, click New project and name it Request decisions.

  2. Replace the default Code.gs contents with the complete example file, including MAX_TABLES and all three function declarations.

  3. In Project Settings, select Show "appsscript.json" manifest file in editor. Return to Editor, replace appsscript.json with the included manifest, and save both files.

  4. Keep MAX_TABLES at 3, seatsPerTable at 2, approved: true, and seats: 5 for the first run.

  5. Select planTrainingRoom in the function menu, click Run, and open Execution log. Select planTrainingRoom rather than either helper so the entrypoint passes their arguments.

The manifest selects the V8 runtime, which supports const and let declarations; see Google's V8 guide.

The execution runs as the signed-in user at the keyboard and requires no Google service consent. Google's authorization guide explains editor execution identity and how Apps Script detects service scopes from project code.

Recognize the result

Expect these four log messages:

Source
Request: Training session
Decision: Ready; tables needed: 3
Decision again: Ready; tables needed: 3
Seat count after planning: 5

Two tables hold four people, so five seats require all three tables. Calling decideRoomRequest twice with the same input returns the same answer without mutating request, leaving request.seats at 5.

Pass arguments into named parameters

The declaration function decideRoomRequest(roomRequest, seatsPerTable) defines a function with two parameters, the local variable names for the values it receives. The braces enclose the function body, which runs when the function is called.

In decideRoomRequest(request, seatsPerTable), the expressions inside the parentheses are arguments: the values supplied for this call. Arguments match parameters by position, not by variable name. The first argument is assigned to roomRequest and the second to seatsPerTable. MDN's functions guide describes function declarations and calls.

Although the caller names its variable request and the helper names its parameter roomRequest, both refer to the same object during the call. Mutating roomRequest.seats would mutate the caller's object as well, so decideRoomRequest only reads its properties. The second argument (2) is a primitive number; reassigning seatsPerTable inside the helper would not affect the caller's variable.

The next call, findTableCount(roomRequest.seats, seatsPerTable), passes two numbers rather than the entire roomRequest object because findTableCount needs only the requested seat count and the table capacity.

planTrainingRoom() takes no parameters, so the editor can run it directly. It calls decideRoomRequest and findTableCount even though their declarations appear lower in Code.gs; MDN explains this behavior under function hoisting. Variables declared with const or let are not hoisted in the same way and must be declared before they are read.

Return a result to the caller

A return value is the result a function call sends back to its caller. When findTableCount reaches return tableCount, the call evaluates to a number. decideRoomRequest stores that number in its local tableCount, formats a decision string, and returns it to planTrainingRoom, which stores it in decision.

Source
planTrainingRoom supplies the request and 2 seats per table
  decideRoomRequest checks approval and the requested 5 seats
    findTableCount returns the number 3
  decideRoomRequest returns "Ready; tables needed: 3"
planTrainingRoom stores that string in decision and logs it

return exits the current function immediately and hands control back to the caller. Inside the loop in findTableCount, return tableCount ends findTableCount while decideRoomRequest continues running. See MDN's return reference.

Logger.log() writes text to the execution log for inspection rather than returning that text to a caller. A function that reaches its closing brace without a return statement returns undefined, so replacing a helper's return with Logger.log() breaks any caller that uses its result.

Keep the returned expression on the same line as return. A newline immediately after return triggers automatic semicolon insertion and returns undefined.

Choose a branch before doing more work

Control flow is the order in which statements execute. An if statement evaluates the condition in parentheses and runs its block when the condition is true; an optional else block runs when the condition is false. See MDN's control-flow guide.

The first check, roomRequest.approved !== true, tests whether approved is anything other than boolean true. When it matches, the helper exits immediately with Needs approval. This early return rejects false, undefined, and the string 'true' before running any seat math.

The next two checks validate the seat count. Number.isInteger(roomRequest.seats) returns true for whole numbers and false for strings such as '5', fractions, NaN, and Infinity. If the check fails, the helper returns Use a whole-number seat count. Next, roomRequest.seats < 1 rejects zero and negative numbers with Request at least one seat.

Only a request that passes all three checks reaches findTableCount. Because each guard returns from the function when its condition is met, an unapproved request returns Needs approval even if its seat count is invalid, and none of the guards require else blocks.

After findTableCount runs, tableCount === null means no table count within MAX_TABLES can seat the group, so that branch returns Too large; room capacity: 6 seats. Its else branch returns the Ready decision with the required table count.

Make the loop advance toward a stop

A loop repeats a block of statements. findTableCount uses a while loop, which tests tableCount < MAX_TABLES before each iteration (MDN while guide).

The counter starts at 0. Each pass assigns tableCount + 1 to tableCount, advancing it from 0 to 1, 2, and 3, and multiplies tableCount * seatsPerTable to check whether capacity >= seatCount.

For the five-seat request:

Table count after the incrementCapacity at two seats per tableIs capacity at least five?Next action
12NoTry the next count.
24NoTry the next count.
36YesReturn 3 from the helper.

Because tableCount increases by 1 on each pass, the first count that fits is the smallest valid table count; a two-seat request returns 1 on the first pass.

The loop always terminates in one of two ways: either capacity >= seatCount succeeds and returns tableCount, or tableCount reaches 3, 3 < 3 evaluates to false, and the function reaches return null. With MAX_TABLES = 3, each call executes the loop body at most three times regardless of the requested seat count.

Every while loop must update state toward its exit condition. Writing tableCount + 1 without assigning the result back to tableCount leaves the counter at 0, causing the loop to repeat until Apps Script terminates the run.

Know which names each function can use

Scope is the region of code where a variable name can be referenced, determined by where the variable is declared. Lifetime is how long a variable's binding exists during execution.

MAX_TABLES is declared at the top level outside any function, so it has global scope and both helpers can read it. Apps Script loads all server .gs files in a project into a single shared global scope (V8 module limitations), so top-level identifiers must be unique across files.

request, seatsPerTable, and decision inside planTrainingRoom are local to that function. decideRoomRequest cannot read request by name; it receives the object through its roomRequest parameter. See MDN's function-scope guide.

Both findTableCount and decideRoomRequest declare a local variable named tableCount. Those are independent variables in separate scopes: findTableCount increments its let tableCount during the loop and returns the resulting number, which decideRoomRequest stores in its own const tableCount.

A block is a section of code enclosed in braces, such as a loop body or an if branch. Variables declared with const or let inside a block are scoped to that block (MDN let reference). In findTableCount, const capacity inside the while loop exists only during that pass and is not visible after the loop ends. By contrast, let tableCount = 0 is declared before the loop so its value persists from one pass to the next; declaring it inside the loop body would reset it to 0 on every iteration.

Local variables disappear when their function call returns, and top-level variables reinitialize on every script execution. Persisting data across executions requires a storage service such as the Properties service.

Separate calculations from effects

A pure helper computes its return value from its inputs without mutating external state or calling external services. Modifying the local counter tableCount inside findTableCount does not affect purity because that variable is created fresh for each call. Both helpers also leave the passed roomRequest object unchanged.

planTrainingRoom performs the side effect of calling Logger.log(). Keeping decision logic in pure helpers and logging in the entrypoint lets you call decideRoomRequest multiple times or across an array of records without producing extra log output on each call.

Request more seats and predict the result

  1. Change seats: 5 to seats: 7 in request. Leave approved: true, seatsPerTable = 2, MAX_TABLES = 3, and the helpers unchanged.

  2. Trace the loop before running: capacities 2, 4, and 6 are all less than 7, so the loop finishes three passes and findTableCount returns null.

  3. Save and run planTrainingRoom. Expect:

Source
Request: Training session
Decision: Too large; room capacity: 6 seats
Decision again: Too large; room capacity: 6 seats
Seat count after planning: 7

  1. Restore seats: 5, save, and rerun to confirm the Ready; tables needed: 3 output.

To test the guard order, set seats to 7 and approved to false. Both decision lines log Needs approval because the approval guard returns before findTableCount is called. Restore approved: true and seats: 5 afterward.

Recover from an editing mistake and rerun

A returned string such as Needs approval or Use a whole-number seat count is a normal return value rather than a thrown exception. For an approved request, seats: '5' returns Use a whole-number seat count and seats: 0 returns Request at least one seat.

Running decideRoomRequest directly from the editor toolbar passes undefined for roomRequest and fails when reading roomRequest.approved; select planTrainingRoom instead. If a logged decision shows undefined after an edit, verify that the helper executes a return statement with its expression on the same line.

Continue to Work with JavaScript arrays to apply helper functions to collections of records.

Project files

Complete source files for this chapter’s examples.

Plan a training room with functions
Code.gs
const MAX_TABLES = 3;

/**
 * @typedef {{title: string, approved: boolean, seats: number}} RoomRequest_
 */

function planTrainingRoom() {
  const request = {
    title: 'Training session',
    approved: true,
    seats: 5,
  };
  const seatsPerTable = 2;
  const decision = decideRoomRequest(request, seatsPerTable);

  Logger.log('Request: ' + request.title);
  Logger.log('Decision: ' + decision);
  Logger.log('Decision again: ' + decideRoomRequest(request, seatsPerTable));
  Logger.log('Seat count after planning: ' + request.seats);
}

/**
 * @param {RoomRequest_} roomRequest
 * @param {number} seatsPerTable
 * @returns {string}
 */
function decideRoomRequest(roomRequest, seatsPerTable) {
  if (roomRequest.approved !== true) {
    return 'Needs approval';
  }
  if (Number.isInteger(roomRequest.seats) !== true) {
    return 'Use a whole-number seat count';
  }
  if (roomRequest.seats < 1) {
    return 'Request at least one seat';
  }

  const tableCount = findTableCount(roomRequest.seats, seatsPerTable);
  if (tableCount === null) {
    const capacity = MAX_TABLES * seatsPerTable;
    return 'Too large; room capacity: ' + capacity + ' seats';
  } else {
    return 'Ready; tables needed: ' + tableCount;
  }
}

/**
 * @param {number} seatCount
 * @param {number} seatsPerTable
 * @returns {number | null}
 */
function findTableCount(seatCount, seatsPerTable) {
  let tableCount = 0;
  while (tableCount < MAX_TABLES) {
    tableCount = tableCount + 1;
    const capacity = tableCount * seatsPerTable;
    if (capacity >= seatCount) {
      return tableCount;
    }
  }
  return null;
}

appsscript.json

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