Every Apps Script check myFunction adds on top of type checking, with the code each one flags, a corrected version, and the quick fix it offers.
Two layers
Type checking, then Apps Script rules.
myFunction type-checks each file against declarations generated from the Apps Script runtime. That layer catches the mistakes below in any service.
Unknown method or property
SpreadsheetApp.getActiveSheet().getValue()
Missing or extra arguments
GmailApp.createDraft("a@example.com")
Wrong argument type
sheet.getRange("1", 1)
Name that is never declared
fetch(url)
Syntax errors
Logger.log("unterminated)
Implicit any and possibly-null results are reported as warnings because ordinary Apps Script code produces them often and still runs.
On top of that, myFunction adds the 82 Apps Script rules listed here. For 48 of them, the editor offers a quick fix or written guidance. The Chrome extension reports every rule. The gas_diagnostics MCP tool reports every rule except MF90006. The VS Code extension reports every rule except MF90006, MF92007, and the HTML page rules, because it does not yet read a project's HTML files or file order. The examples on this page come from the language server's tests, which check that each rule flags the first example and not the corrected one.
In the Chrome extension, the manifest and OAuth scope rules run when Show "appsscript.json" manifest file in editor is on in Project Settings, because the extension reads the manifest from the editor. In HTML files, the extension marks HTML page rule problems but does not offer their quick fixes yet.
Simple trigger calls a service that needs authorization
onOpen, onEdit, and onSelectionChange run without authorization. Calls to GmailApp, MailApp, UrlFetchApp, DriveApp, CalendarApp, or ContactsApp, or opening another file by ID or URL, fail when the trigger fires.
Quick fix: None. The message explains the change to make.
// Run from an installable "On edit" trigger.functionnotifyOnEdit(e) {
MailApp.sendEmail("team@example.com", "Sheet edited", e.range.getA1Notation());
}
Menu item or trigger names a function that does not exist
Menu.addItem() and ScriptApp.newTrigger() take the function name as text, and Apps Script only looks it up when the item is clicked or the trigger fires. A misspelled name, or one written with parentheses, fails then with "Script function not found". The rule reports names that are close to a function in the project, since a name with no near match may live in a file that is not loaded.
Quick fix: Change the name to the matching function, or drop the parentheses.
A function named onOpen, onEdit, onSelectionChange, or onInstall already runs as a simple trigger. An installable trigger that points at it makes the function run from both triggers, for example twice on every edit.
Quick fix: None. The message explains the change to make.
When a user installs an editor add-on, only onInstall() runs, so the add-on menu stays missing until the file is reopened. Google's add-on samples call onOpen(e) from onInstall(e).
Quick fix: Add onOpen(e) as the first line of onInstall().
getUi() and Browser dialogs need an open Sheets, Docs, Slides, or Forms editor. When doGet, doPost, a time-driven trigger, or a form-submit trigger reaches them, directly or through other functions, the call throws. Calls inside a try block are skipped.
Quick fix: None. The message explains the change to make.
A web app deployed to execute as you returns an empty string from Session.getActiveUser().getEmail() unless the visitor is in your Google Workspace domain. Deploy it to execute as the user accessing it when you need the visitor's email.
Quick fix: None. The message explains the change to make.
Simple triggers such as onOpen run without authorization, so creating a trigger from one fails. From doGet or doPost, each request adds another trigger until the project hits its limit of 20 per user. Create triggers from a menu item or a function you run once, or check getProjectTriggers() first.
Quick fix: None. The message explains the change to make.
Rows and columns start at 1, and a range needs at least one row and one column. Sheet.getRange(0, 1) or a literal 0 for numRows throws "The coordinates or dimensions of the range are invalid."
Quick fix: None. The message explains the change to make.
setValues() and the other 2D setters need exactly one row of data per range row and one value per column. When getRange() has literal dimensions or A1 notation and the data is an array literal, a different row count throws "The number of rows in the data does not match the number of rows in the range." A different column count, or rows of uneven length, throws the same error for columns.
Quick fix: None. The message explains the change to make.
getRange(2, 1, sheet.getLastRow() - 1, n) asks for zero rows on a sheet that holds only a header, and getRange() throws "The number of rows in the range must be at least 1." The rule stays quiet when the function compares the last row before reading. getLastColumn() counts are checked the same way.
Quick fix: Return early when the sheet has nothing past the header, in functions that return no value.
The third argument of getRange() is a number of rows, not the last row. Starting at row 2 with getLastRow() rows reads one row past the data, and the extra rows come back as empty strings.
deleteRow() moves every later row up by one. In a loop that counts up, the row after each deleted one is never visited. The rule stays quiet when the loop corrects the index, for example with i--.
Quick fix: Reverse the loop so it counts down.
Flagged1 changed line
functionremoveDone() {
const sheet = SpreadsheetApp.getActiveSheet();
const values = sheet.getDataRange().getValues();
for (let i = 1; i < values.length; i++) {
if (values[i][0] === "Done") sheet.deleteRow(i + 1);
}
}
Not flagged1 changed line
functionremoveDone() {
const sheet = SpreadsheetApp.getActiveSheet();
const values = sheet.getDataRange().getValues();
for (let i = values.length - 1; i >= 1; i--) {
if (values[i][0] === "Done") sheet.deleteRow(i + 1);
}
}
for...in yields each array index as a string, so with rows from getValues(), i + 2 is "02" and a getRange() row computed from it is wrong. It can also visit properties that are not elements. Use a counting for loop, for...of, or forEach().
Quick fix: Rewrite as a counting for loop over the array's length.
Flagged1 changed line
functionlogRows() {
const rows = SpreadsheetApp.getActiveSheet().getDataRange().getValues();
for (var i in rows) {
Logger.log("Row " + (i + 1) + ": " + rows[i][0]);
}
}
Not flagged1 changed line
functionlogRows() {
const rows = SpreadsheetApp.getActiveSheet().getDataRange().getValues();
for (var i = 0; i < rows.length; i++) {
Logger.log("Row " + (i + 1) + ": " + rows[i][0]);
}
}
Utilities.formatDate() patterns follow Java SimpleDateFormat. YYYY is the week year, which is off by one for some dates around January 1. DD is the day of the year, and mm is minutes, so dd/mm/yyyy prints the minutes where the month should be.
DriveApp.getFilesByName() and similar calls return an iterator. Calling next() when nothing matches throws, so a renamed or missing file stops the script with an error that does not name the file.
Quick fix: Store the iterator and throw a clear error when hasNext() is false.
With muteHttpExceptions: true, UrlFetchApp returns 4xx and 5xx responses instead of throwing. Code that reads the body without calling getResponseCode() treats an error page as data.
Quick fix: Throw when the status is 400 or above, with the response body in the message.
tryLock() returns false when another run holds the lock past the timeout. If the result is not checked, the code after it runs without the lock it was meant to have.
Quick fix: Use waitLock(), which throws when the lock is not available in time.
A lock from waitLock() or tryLock() is held until the script finishes when the function never calls releaseLock(). Other runs wait for that whole time. Release it in a finally block so an error does not keep it held either.
Quick fix: None. The message explains the change to make.
A string that looks like an API key, access token, or bearer token is readable by anyone who can view or copy the project, and it stays in the version history after you delete it. Keep it in Script Properties and read it with PropertiesService. The message never repeats the value.
Quick fix: Read the value from Script Properties; add the property under Project Settings and rotate the old key.
A value placed after ?name= or &name= in a fetched URL goes in as typed. A space, &, #, or accented letter in it breaks the request or changes which parameters the server sees. Wrap the value in encodeURIComponent().
Quick fix: Wrap the value in encodeURIComponent().
A time zone written into Utilities.formatDate() stays fixed when the project's time zone in appsscript.json changes, and it disagrees with it now. Session.getScriptTimeZone() follows the manifest. UTC and GMT offsets are not flagged.
UiApp, DocsList, ScriptDb, FinanceApp, and ContactsApp no longer run. Code that calls them fails, even where the editor still knows their names. Each message names the replacement, such as HtmlService for UiApp and the People advanced service for ContactsApp.
Quick fix: None. The message explains the change to make.
Session.getTimeZone(), Session.getUser(), ScriptApp.getProjectKey(), Utilities.jsonParse(), and Utilities.jsonStringify() are deprecated. Each has a replacement that takes the same arguments.
Calling getValue() or another single-cell getter on each pass of a loop, or in a forEach() or map() callback, makes one service call per cell. Reading the block once with the plural getter is much faster.
Quick fix: Read the range once with the plural getter before the loop, when the loop shape allows it.
Calling setValue() or another single-cell setter on each pass of a loop, or in a forEach() or map() callback, makes one service call per cell. Collecting a 2D array and writing it once is much faster.
Quick fix: Collect the values and write the range once with the plural setter, when the loop shape allows it.
GmailApp.search(query), getInboxThreads(), and similar calls without a start and max return every matching thread, and Google notes they fail when the threads are too large to handle. Pass a start and max and page through the results.
openById(), getSheetByName(), getFileById(), and similar lookups return the same object when their argument does not change. Inside a loop they repeat the service call on every pass.
Quick fix: Move the const above the loop.
Flagged1 changed line
functionstampRows(rows) {
const ss = SpreadsheetApp.getActive();
for (const row of rows) {
const log = ss.getSheetByName("Log");
log.getRange(row, 2).setValue(newDate());
}
}
Not flagged1 changed line
functionstampRows(rows) {
const ss = SpreadsheetApp.getActive();
const log = ss.getSheetByName("Log");
for (const row of rows) {
log.getRange(row, 2).setValue(newDate());
}
}
A for or while condition runs before every pass, so sheet.getLastRow() there asks Sheets again each time. The rule stays quiet when the loop body writes to the sheet or calls project functions, since the value may then change.
Quick fix: Read the value into a const before the loop.
Properties read or written one key at a time in a loop
getProperty() and setProperty() each call the Properties service. In a loop, read every value once with getProperties() and write changes together with setProperties().
Quick fix: None. The message explains the change to make.
Flagged4 changed lines
functionsaveTotals(totals) {
const props = PropertiesService.getScriptProperties();
for (const name ofObject.keys(totals)) {
props.setProperty(name, String(totals[name]));
}
}
Not flagged3 changed lines
functionsaveTotals(totals) {
const values = {};
for (const name ofObject.keys(totals)) values[name] = String(totals[name]);
PropertiesService.getScriptProperties().setProperties(values);
}
Apps Script allows 100 email recipients a day on a consumer account and 1,500 on Google Workspace. A loop that sends mail without checking MailApp.getRemainingDailyQuota() fails partway through the list once the quota runs out.
Quick fix: None. The message explains the change to make.
Flagged0 changed lines
functionnotify(addresses) {
for (const address of addresses) {
MailApp.sendEmail(address, "Report ready", "The weekly report is ready.");
}
}
Not flagged1 changed line
functionnotify(addresses) {
for (const address of addresses) {
if (MailApp.getRemainingDailyQuota() === 0) thrownewError("Daily email quota used up at " + address);
MailApp.sendEmail(address, "Report ready", "The weekly report is ready.");
}
}
A range such as A2:C or A:A runs to the last row of the sheet, not the last row with data. getValues() then returns a row for every empty row too, often thousands of them, and loops over the result do the same.
Apps Script runs .gs files on Google's servers, which have no window, document, localStorage, fetch(), setTimeout(), or alert(). Each one throws a ReferenceError. The message names the Apps Script service to use instead.
Quick fix: Use UrlFetchApp.fetch() for fetch() and getUi().alert() for alert().
Apps Script runs every file's top-level code before each function call, including simple triggers and custom functions, which cannot use every service. A service call there runs on every execution, and when it fails, every function in the project fails with it.
Quick fix: None. The message explains the change to make.
Apps Script loads files in the order the editor lists them. A class, let, or const declared in a later file does not exist yet while an earlier file's top-level code runs, so using it there throws "is not defined". Code inside functions runs later and is fine. The check runs once the editor knows the file order.
Quick fix: None. The message explains the change to make.
createHtmlOutputFromFile() and createTemplateFromFile() look up the file by its exact name, without .html. A name that matches no HTML file in the project throws when the line runs. The check runs once the editor knows every file in the project.
Quick fix: Change the name to the closest HTML file in the project.
createHtmlOutputFromFile() serves the file as it is, so <? ?> scriptlets in it show on the page as text. A file with scriptlets needs createTemplateFromFile(...).evaluate().
Quick fix: Switch to createTemplateFromFile(...).evaluate().
<?!= prints a value without escaping it. When the value comes from a user, a sheet, or e.parameter, a visitor can put their own HTML or script on the page. <?= escapes the value; keep <?!= for including another file's HTML.
An include() helper returns another file's HTML. Printing it with <?= escapes that HTML, so the page shows the markup as text instead of the styles or script it holds.
google.script.run calls a missing or private function
google.script.run can call only top-level functions of the project whose names do not end in an underscore. A typo or a private function fails in the browser with a 'not a function' error. The check runs once the editor knows every file in the project.
google.script.run passes numbers, strings, booleans, arrays, and plain objects back to the page. A Date is not one of them, so the call fails and the success handler never runs. Return an ISO string or a formatted date instead.
Quick fix: None. The message explains the change to make.
Call needs an OAuth scope the manifest does not list
When appsscript.json lists oauthScopes, Apps Script grants only those scopes. A call whose reference page requires a scope missing from the list fails with a permission error at runtime. The check uses the Authorization section of each method's reference page.
Quick fix: Add one of the accepted scopes to oauthScopes in appsscript.json.
Every scope in oauthScopes appears on the consent screen, and sensitive scopes can require app verification. A built-in service scope that no call in the project accepts is likely left over. Projects with libraries, advanced services, or add-on or Chat configuration are skipped, since those can need scopes the code does not show.
When appsscript.json lists urlFetchWhitelist, deployed versions can fetch only URLs under those prefixes. A literal URL passed to UrlFetchApp that no prefix covers fails once the script is deployed.
Quick fix: None. The message explains the change to make.
Without runtimeVersion, or with "STABLE" or "DEPRECATED_ES5", Apps Script runs the legacy Rhino runtime, which lacks modern JavaScript such as classes and arrow functions in some contexts. myFunction checks code as V8.
A manifest field has the wrong JSON type or a value Apps Script does not accept, such as a web app access level, an exceptionLogging setting, a time zone, or an oauthScopes entry that is not a scope URL.
Quick fix: None. The message explains the change to make.
Each entry in dependencies.enabledAdvancedServices needs serviceId, userSymbol, and version, and each library needs libraryId, userSymbol, and version. Two entries cannot share a userSymbol.
Quick fix: None. The message explains the change to make.
Each allowlist prefix must use https://, name a full domain, and have a non-empty path such as a trailing slash. A single * can only stand for the leading subdomain.
Quick fix: None. The message explains the change to make.
GmailApp needs the scope to read, send, and delete all of a user's mail. When a project only sends mail with GmailApp.sendEmail() and uses no from address, MailApp.sendEmail() takes the same arguments and asks only to send mail as the user.
Quick fix: Send with MailApp.
Flagged1 changed line
functionnotify(to) {
GmailApp.sendEmail(to, "Report ready", "The weekly report is in the shared folder.");
}
Not flagged1 changed line
functionnotify(to) {
MailApp.sendEmail(to, "Report ready", "The weekly report is in the shared folder.");
}
When every call that relies on a listed scope also accepts a narrower one, such as drive.readonly instead of drive, listing the narrower scope gives the script less access and shows users a smaller request. Projects with libraries, advanced services, or add-on configuration are skipped.
Quick fix: Replace the scope with the narrower one.
A web app that executes as the user accessing it needs a signed-in user to run as, so it cannot allow anonymous access. Deploy it to execute as you, or require visitors to sign in.
Quick fix: Run the web app as you, or require sign-in.
Advanced services such as Drive, Sheets, or Gmail exist only after you add them under Services, which lists them in dependencies.enabledAdvancedServices. Until then, code that uses one throws "is not defined". The check runs when the editor can read appsscript.json.
Quick fix: Add the service to dependencies.enabledAdvancedServices in appsscript.json.
Code that calls OAuth2, OAuth1, Cheerio, FirebaseApp, or BetterLog expects the open-source library by that identifier. Until the library is added under Libraries, the identifier is not defined. The check runs when the editor can read appsscript.json.
Quick fix: None. The message explains the change to make.
Flagged1 changed line
// appsscript.json lists no librariesfunctiongetService() {
returnOAuth2.createService("github");
}
Not flagged1 changed line
// appsscript.json lists the OAuth2 libraryfunctiongetService() {
returnOAuth2.createService("github");
}
JSDoc annotations such as GoogleAppsScript.Events.DoPost or GoogleAppsScript.Spreadsheet.Range come from DefinitelyTyped's @types/google-apps-script package. Replacing them with myFunction's global types (Events.DoPost, SpreadsheetApp.Range) uses the runtime-verified declarations directly.
Quick fix: Replace the GoogleAppsScript.* type with the matching global Apps Script type.
Flagged1 changed line
/** @param {GoogleAppsScript.Events.DoPost} e */functiondoPost(e) {
returnContentService.createTextOutput(e.postData.contents);
}
Not flagged1 changed line
/** @param {Events.DoPost} e */functiondoPost(e) {
returnContentService.createTextOutput(e.postData.contents);
}