Skip to content

Basics

Common Apps Script errors and what they mean

Apps Script error messages name the failure directly, from TypeError: Cannot read properties of null (reading 'getRange') to ReferenceError and quota limits. Find the exact message below for the cause and fix.

1 of 10

TypeError: Cannot read properties of null (reading 'getRange')

This says two things. Something in your code was null, which means empty, and the script then tried to call getRange() on that empty thing. In a Sheets script the empty thing is almost always the result of getSheetByName().

getSheetByName() returns null when no tab has that exact name. The name is case sensitive, and a trailing space counts, so "Customers " is not the same as "Customers".

Check the tab name at the bottom of the spreadsheet and make the code match. Then add a check, so the next time the message names the tab instead of the method. The same wording with a different method inside the brackets means the same thing: whatever came before the dot was null.

Stop with a clear message instead of a TypeError
function readCustomersTab() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Customers');
  if (!sheet) {
    throw new Error('No tab named Customers. Check the spelling on the tab at the bottom of the spreadsheet.');
  }

  Logger.log('Customers holds ' + sheet.getLastRow() + ' rows of content.');
}

2 of 10

TypeError: Cannot read properties of undefined (reading 'range')

Trigger functions such as onEdit(e) and an on form submit handler are handed an event object when they run. The letter e holds details of what happened: e.range for the cell that was edited, e.values for the answers in a form response.

When you press Run in the editor, nobody edited anything and nobody submitted anything. Apps Script calls the function with no event object at all, so e is undefined and e.range fails.

This is not a bug in your code. Test it by editing a cell in the sheet, or by submitting the form, and let the trigger call the function. If you want a button to press in the editor, write a second function that builds the parts of the event you need and passes them in.

Guard the handler and test it from a second function
/** @param {Events.SheetsOnEdit} e */
function onEdit(e) {
  if (!e || !e.range) {
    Logger.log('Run this by editing a cell in the sheet, not with the Run button.');
    return;
  }
  Logger.log('Edited ' + e.range.getA1Notation() + ' on the tab ' + e.range.getSheet().getName());
}

function testOnEdit() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Customers');
  if (!sheet) throw new Error('No tab named Customers.');
  onEdit({
    authMode: ScriptApp.AuthMode.NONE,
    range: sheet.getRange('A2'),
    source: SpreadsheetApp.getActiveSpreadsheet()
  });
}

3 of 10

Exception: The coordinates or dimensions of the range are invalid

getRange(row, column, numRows, numColumns) needs four numbers that describe a real rectangle. Every one of them has to be 1 or more. A row count of 0 or of minus 3 is not a rectangle, so the run stops here.

An empty tab is the usual cause. On a tab that holds only a header row, getLastRow() returns 1, so lastRow - 1 is 0 rows. On a tab with nothing at all it returns 0, and the count goes negative.

Work out the row count first, and return early when it is not at least 1. The script then does nothing on an empty tab instead of failing.

Return early when the tab has no data rows
function countCustomerRows() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Customers');
  if (!sheet) throw new Error('No tab named Customers.');

  const firstDataRow = 2;
  const numRows = sheet.getLastRow() - firstDataRow + 1;
  if (numRows < 1) {
    Logger.log('Customers has a header but no data rows yet, so there is nothing to read.');
    return;
  }

  const values = sheet.getRange(firstDataRow, 1, numRows, 3).getValues();
  Logger.log('Read ' + values.length + ' rows.');
}

4 of 10

Exception: The number of rows in the data does not match the number of rows in the range

setValues() writes a rectangle, and the array you give it must be exactly the size of the range. Twelve rows of two columns means twelve inner arrays, each holding two items.

It usually breaks when the range is sized from the sheet but the array is built from a shorter list, for example after rows were skipped. It also breaks when a row is a plain value instead of an array holding one value, so ["Checked"] is right and "Checked" is not.

Size the range from the array rather than the other way round, and make sure every inner array has the same number of items. The column version of this message says "columns" instead of "rows" and means the same thing.

Size the range from the array you built
function writeStatusColumn() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Customers');
  if (!sheet) throw new Error('No tab named Customers.');

  const firstDataRow = 2;
  const numRows = sheet.getLastRow() - firstDataRow + 1;
  if (numRows < 1) return;

  const statuses = [];
  for (let index = 0; index < numRows; index += 1) {
    statuses.push(['Checked']);
  }

  sheet.getRange(firstDataRow, 4, statuses.length, 1).setValues(statuses);
  Logger.log('Wrote ' + statuses.length + ' rows into column D.');
}

5 of 10

Exception: You do not have permission to call SpreadsheetApp.getUi

You may see the same problem worded as "Cannot call SpreadsheetApp.getUi() from this context". Both mean the code tried to put something on screen when there is no screen to put it on.

getUi() gives you menus, alerts, and prompts. It works when a person has the spreadsheet open and started the code, for example by clicking your custom menu item. It does not work inside a time driven trigger that fires overnight, inside a custom function typed into a cell, or in a script somebody else set running.

Swap the alert for Logger.log() so the message lands in the execution log, or email yourself the result. Keep getUi() for the code paths a person starts by hand.

Log the result instead of alerting when nobody is watching
function reportRowCountForATrigger() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Customers');
  if (!sheet) throw new Error('No tab named Customers.');

  const message = 'Customers has ' + sheet.getLastRow() + ' rows.';
  Logger.log(message);
}

6 of 10

Exceeded maximum execution time

One run of a script may take up to six minutes at the time of writing. When it goes past that, Apps Script stops it and records this message. Anything already written to the sheet or already sent stays done, so a script stopped halfway can leave half the rows updated.

The usual cause is calling a Google service inside a loop, such as getValue() on every cell or sendEmail() on every one of 400 rows. Each call is a round trip to Google servers. Read the whole block once with getValues() and write once with setValues(), and the same job often finishes in seconds.

When there is more work than six minutes allows, split it. Handle part of the list, save the row number to continue from, and let the next run carry on from there.

7 of 10

Exception: Service invoked too many times for one day: email

This is a daily quota. A quota is a cap on how much your scripts may do in one day, counted per Google account rather than per script. The word after the colon names which cap you filled, and email is the one people meet first.

At the time of writing a free Gmail account may send to 100 recipients a day from Apps Script, and a Google Workspace account may send to 1,500. Recipients are counted, not messages, so one email addressed to 20 people uses 20 of them.

There is no way to raise it or reset it early. Wait for the daily reset, then send fewer per run, and mark each row as sent so a second run does not send it again. MailApp.getRemainingDailyQuota() tells you how much room is left before you start.

Check what is left before sending a batch
function checkRemainingEmailQuota() {
  const remaining = MailApp.getRemainingDailyQuota();
  Logger.log('This account can still send to ' + remaining + ' recipients today.');
  if (remaining < 10) {
    Logger.log('Fewer than 10 left. Wait for the daily reset before sending more.');
  }
}

8 of 10

ReferenceError: foo is not defined

The script used a name it has never seen. In the real message foo is replaced by the name you used, for example "ReferenceError: sheeet is not defined".

Three causes cover nearly all of these. A typo in a variable or function name. A variable declared inside one function and then used in another, where it does not exist. Or a function that lives in a different Apps Script project, because separate projects do not share code.

Read the name in the message, then find where you meant to declare it.

  • Check the spelling, including capital letters. sheetName and sheetname are two different names.
  • A const or let declared inside a function cannot be reached from another function. Pass it in as a parameter, or declare it at the top of the file.
  • Functions in another Apps Script project are not visible. Copy the function into this project, or add the other project as a library.
  • A word without quotes is a variable name. getSheetByName(Customers) looks for a variable called Customers, while getSheetByName('Customers') looks for the tab.

9 of 10

Exception: Authorization is required to perform that action

A script may only touch your data after you have approved it. The approval covers a specific list of permissions, worked out from the services the code uses. Sheets and Gmail are two different permissions.

This message usually appears on a trigger rather than while you are in the editor. Something new was added to the code, for example the first GmailApp line in a script that only used Sheets before. That needs a permission nobody has approved yet, and a trigger has no way to show you the approval dialog.

Run the function once by hand to clear it. Open the editor, pick the function from the list at the top, press Run, and work through "Review permissions" and Allow. After that the trigger runs cleanly.

  • A trigger runs as the person who created it. If the trigger is not yours, that person has to run the function once and approve it.
  • Check which account the dialog is offering. A browser signed in to more than one Google account often picks the wrong one.
  • If it still fails after approval, delete the trigger and create it again so it picks up the new permissions.

10 of 10

#NAME? Unknown function in a spreadsheet cell

Google Sheets displays #NAME? when it evaluates a formula in a cell and cannot find a built-in or custom function matching the name you typed, such as =DOUBLE_(A2).

In Apps Script, any function whose name ends with an underscore is treated as private. Private functions are hidden from Google Sheets, so removing the trailing underscore lets Sheets discover the formula.

Custom functions must also live in a script bound to the spreadsheet, not a standalone project. Save the project before typing the formula into a cell. The @customfunction JSDoc tag only adds the name to autocomplete, so a missing tag does not cause #NAME?.

Remember

The short version

  • Read the whole message. The words after the colon name the thing that went wrong.
  • A null value before a method call almost always means getSheetByName() did not find a tab with that name.
  • An undefined event object means a trigger function was started with the Run button instead of by the trigger.
  • Range errors are size errors, so check the row and column counts on both sides.
  • Quota and authorization errors are not bugs in the code, and the fix is a change in how much the script does or in how it was approved.

Keep going

Put it to work

All basics