// Sheets · Apps Script

Fix "Cannot read properties of undefined (reading 'range')" in Apps Script.

The "Cannot read properties of undefined (reading 'range')" error in Apps Script almost always means you ran a trigger handler directly from the editor, where the event object is undefined. Here is how to debug trigger functions properly using a test wrapper.

I ran my onEdit function from the Apps Script editor and got "Cannot read properties of undefined (reading 'range')" — I need to understand why and how to test trigger code correctly.

The script

copy · paste · trigger
testOnEdit.gs
Apps Script
// Test wrapper — run this from the editor instead of onEdit directly
function testOnEdit() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sheet1');
  var fakeEvent = {
    range: sheet.getRange('B2'),
    value: 'test',
    oldValue: '',
    source: SpreadsheetApp.getActiveSpreadsheet()
  };
  onEdit(fakeEvent);
}

function onEdit(e) {
  var range = e.range;
  if (range.getColumn() !== 2) return;
  range.setBackground('lightyellow');
}

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

Walkthrough

Why the Run button breaks trigger handlers

When you click Run (or press Ctrl+R) in the Apps Script editor, it calls your function with zero arguments. For a normal utility function that is fine. For an event-driven handler like onEdit, onFormSubmit, or onSelectionChange, the first parameter is an event object the Sheets runtime constructs and passes automatically — it contains range, value, oldValue, source, and a few other fields depending on the trigger type.

Run in the editor provides none of that. The parameter e arrives as undefined, so the first line that touches e.range (or e.value, or e.namedValues) throws 'Cannot read properties of undefined'. The function is not broken. The launch method is wrong.

Build a test wrapper that fabricates the event

The practical fix is a second function — call it testOnEdit or debugOnEdit — that you run from the editor instead. It builds a plain JavaScript object that matches the shape of the real event, then calls your handler with it. The snippet above shows the minimum viable shape for a Sheets onEdit event: a real Range object from getRange(), a value string, an oldValue string, and source pointing to the spreadsheet.

I keep a file called testHarness.gs in every project with one wrapper per trigger handler. It costs two minutes to write and saves the twenty-minute loop of 'deploy, edit a cell, read logs, repeat' every time you touch trigger logic. The key constraint is that range must be a real Range object — you cannot pass a string like 'B2' directly because the handler will call methods like getColumn() on it.

For onFormSubmit the fabricated object looks different: it has namedValues (an object mapping question titles to arrays of answer strings), values (a flat array), and range pointing to the new response row. Check the Apps Script documentation for the exact shape of each trigger's event object before writing your wrapper.

When to install the real trigger and edit for real

Test wrappers cover the common path, but some bugs only surface in the live trigger context — authorization scope differences, the difference between a simple trigger (onEdit) and an installable one (ScriptApp.newTrigger), or edge cases where the event object contains fields that are hard to fake accurately.

For those cases, install the trigger via the Triggers panel (clock icon in the left sidebar), open the sheet, make an edit, then read the execution log under Executions. The log shows the full stack trace with line numbers. This round-trip takes longer but is the only way to observe what the real runtime event actually looks like — if you log JSON.stringify(e) inside onEdit during a real trigger fire, you will see every field Sheets populates, which is useful for building a more accurate fake event later.

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 this error mean my onEdit function has a bug?
Usually not. The error means onEdit received undefined instead of an event object, which happens when you run the function directly from the editor. The function itself is typically fine; the problem is the launch path.
My onEdit works when the trigger fires but crashes in the editor — is that expected?
Yes. That is exactly the gap this page describes. The editor's Run button passes no arguments; the real trigger runtime passes a populated event object. Use a test wrapper to bridge them during development.
What fields does the onEdit event object actually contain?
The core fields are range (a Range object for the edited cell or cells), value (the new value as a string), oldValue (the previous value, absent on newly populated cells), source (the Spreadsheet object), and user (the User object, only in installable triggers). Simple triggers also omit user for privacy reasons.
Can I use console.log to inspect the real event object?
Inside Apps Script, use Logger.log(JSON.stringify(e)) or console.log(JSON.stringify(e)) — both land in the Executions log. Run that inside a real trigger fire (edit a cell with the trigger installed) and you will see the full event shape, which makes writing accurate test wrappers much easier.