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.
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.
/** @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.
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.
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.
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.
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.
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
- Task guide Send an email for each row in a Google Sheet
Use one row per person, then let the script send only the rows that have not already been marked as sent.
- Task guide Add a timestamp when a row changes
Change a status cell and the script stamps the same row with the current date and time. There is nothing to install beyond saving the script.
- Task guide Run an Apps Script function every day
Run the setup function once, then Apps Script calls the reminder function each day around your chosen hour.
- Basics Execution log, Logger.log, and debugging
Most Apps Script debugging is printing a value with Logger.log or console.log and reading the error line. This page shows where the output goes and how to trace a failure.
- Basics Rows, columns, and 2D arrays in Sheets scripts
A range is a rectangle of cells. getValues() hands you that rectangle as an array of rows, and each row is an array of cell values.
- Basics Google Apps Script quotas and limits
Google caps how long one run may take and how much your scripts may do in a day. Knowing the caps changes how you write anything that touches a lot of rows.
- Basics Custom functions in Google Sheets
A Google Sheets custom function runs in a silent sandbox with its own rules: 30-second timeouts, raw 2D arrays instead of Range objects, no personal services, and no cell edits.
- Diagnostic TypeError: Cannot read properties of null (reading 'getRange')
Why getSheetByName() returned null one line earlier and how to guard the lookup before calling getRange().
- Diagnostic ReferenceError: X is not defined
How to tell a misspelled variable name from a function scope gap, an unsaved file, or a cross-file load order problem.
- Diagnostic The number of rows in the data does not match the number of rows in the range
Why a setValues() range sized from a separate count disagrees with the 2D array and how to size from values.length.
- Book chapter Debug executions and recover from limits
Step through a request audit, trace failures in execution history and logs, and recover bounded work without blind retries.