Skip to content
// Sheets · Apps Script

getActiveSheet vs getSheetByName in Google Sheets.

Learn when getActiveSheet silently returns the wrong tab in time-driven triggers and web apps, and how to pin sheets by name or ID so your script runs on the right data every time.

I wrote a script that works fine when I run it manually, but it reads the wrong sheet when triggered automatically.

The script

copy · paste · trigger
pinSheet.gs
Apps Script
// Pin a sheet by name; fall back to sheet ID for rename safety.
// getActiveSheet() is unreliable outside the browser UI.

function getSheetSafe(name, fallbackId) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const byName = ss.getSheetByName(name);
  if (byName) return byName;

  // If the tab was renamed, find it by its stable numeric ID.
  const sheets = ss.getSheets();
  for (const sheet of sheets) {
    if (sheet.getSheetId() === fallbackId) return sheet;
  }
  throw new Error('Sheet not found: ' + name + ' (id=' + fallbackId + ')');
}

function dailyReport() {
  const data = getSheetSafe('Raw Data', 123456789);
  const output = getSheetSafe('Summary', 987654321);
  Logger.log('Rows: ' + data.getLastRow());
}

Need a variant? Gnaw writes a custom version from one sentence — fields, triggers, edge cases handled.

Walkthrough

Why "active" means nothing to a trigger

getActiveSheet() is a UI concept. It returns whichever tab the user currently has selected in the browser. When you run a function from the Apps Script editor, that user context exists and the call works as expected.

Time-driven triggers, installable onEdit triggers fired by a different user, and doGet/doPost web app handlers all execute in a headless context: no browser, no cursor, no selected tab. In that situation getActiveSheet() does not throw an error. It silently returns the first tab in the spreadsheet, which is whatever sheet happens to be in position 0 — often a cover sheet, a changelog, or something you created years ago.

The first time I hit this, a nightly summary script had been quietly aggregating data from a 'Changelog' tab for two weeks before anyone noticed the numbers were wrong. The script ran clean, no errors, just wrong data. That silence is what makes this bug expensive.

Pinning by name, and when that breaks

getSheetByName('Raw Data') solves the trigger problem. It searches all tabs by their display name and returns the matching Sheet object, or null if nothing matches. Null is better than silently wrong — you can check for it and throw a meaningful error.

The catch is renaming. Non-technical collaborators rename tabs constantly. 'Raw Data' becomes 'Raw Data 2024', the script returns null, and your trigger starts throwing NullPointerException on sheet.getRange(). The error is easy to diagnose but the rename window can be days long before anyone looks at the logs.

The durable fix is to pair the name lookup with a fallback to getSheetId(). Every Sheet has a numeric ID assigned at creation time that survives renaming, reordering, and even copy-to-another-spreadsheet operations. You can find a sheet's ID in the URL when you click on its tab: the gid= parameter. Store that number alongside the expected name. If the name lookup fails, scan all sheets for the matching ID. If that also fails, throw with both pieces of information so whoever reads the log knows exactly what went wrong.

getActiveSpreadsheet() is different — that one is fine

A common point of confusion: getActiveSpreadsheet() does work reliably in triggers, as long as the script is bound to a specific spreadsheet (opened via Tools > Script editor from inside that file). Bound scripts have a container spreadsheet baked in; getActiveSpreadsheet() resolves to it regardless of UI state.

Standalone scripts — created at script.google.com and not bound to any file — cannot use getActiveSpreadsheet() at all. They have no implicit container. You must use SpreadsheetApp.openById('your-spreadsheet-id') or SpreadsheetApp.openByUrl() with the full URL.

So the safe pattern in a bound script is: getActiveSpreadsheet() to get the workbook, then getSheetByName() plus a sheet-ID fallback to get the tab. Never chain getActiveSpreadsheet().getActiveSheet() in code that runs outside the editor.

Want a custom version?

Describe your sheet and the rule you want. Gnaw writes the Apps Script — fields, triggers, edge cases — in one shot.

FAQ

4 questions
Does getActiveSheet() always return the first sheet in a trigger?
In practice, yes — it returns the sheet at index 0 (the leftmost tab). The Apps Script documentation says the result in a no-UI context is undefined, so you cannot even rely on index 0 consistently across runtimes. Treat the return value as arbitrary and never use it in a trigger.
Where do I find a sheet's numeric ID to use as a fallback?
Click the tab in the Sheets UI. The URL updates to include a gid= parameter, for example #gid=123456789. That number is the sheet ID. Alternatively, run Logger.log(SpreadsheetApp.getActiveSpreadsheet().getSheets().map(function(s){ return s.getName() + ': ' + s.getSheetId(); })) once from the editor and read the log.
What is the difference between getSheetByName returning null and throwing an error?
getSheetByName returns null silently when no tab matches. Calling any method on that null value (like .getRange()) then throws a TypeError: Cannot call method getRange of null. Checking for null immediately and throwing your own descriptive error puts the real cause in the log rather than a generic TypeError on a line far from the actual problem.
Can I use getActiveSheet() safely in an onOpen trigger?
onOpen runs with a UI context — it fires when a user opens the spreadsheet in a browser. getActiveSheet() will return the tab that was active when the file last closed, which is usually the first tab but not guaranteed. For anything consequential in onOpen, still prefer getSheetByName().
// one good script a week

Get a working Apps Script snippet in your inbox, weekly.