Your script threw TypeError: Cannot read properties of undefined (reading '0') and the account manager Slack'd "is it broken?" That's on the error message, not on them. Bootstrapper tools get used by people who shouldn't need to read stack traces. Here's how to turn failures into actions: which row, which field, what to do next — in the toast, the cell note, and the email.
TL;DR
- Throw / show named, plain-language errors: what failed + which row/field + next step.
- Prefer
SpreadsheetApp.getUi().alert/toastfor menu actions; email for triggers. - Never surface raw
err.stackto operators (log it; don't modal it). - Validate early (headers, ids, required cells) before half-writing data.
- One pattern:
userMessagefor humans,logRow+ optionalemailErrorAlertfor you.
Using the Apps Script editor a lot? NitroGAS drops free themes & snippets right into script.google.com — optional Co-Pilot when you want a boost.
Why do default Apps Script errors fail humans?
Because they answer a different question. Eng wants "where in the code." Ops wants "which client row and what do I type."
Bad:
Exception: Unexpected error while getting the method or property openById on object SpreadsheetApp.
Better:
Couldn't open the client workbook on row 14.
Check column "Workbook URL" — paste a full Google Sheets link or file id, then run again.
Same failure. Second one gets fixed without you.
What does an actionable error contain?
Four parts, short:
- Outcome — what didn't happen ("Couldn't create the client tab")
- Pointer — row, sheet, field, or job id
- Likely cause — in operator vocabulary ("name already used", "link missing")
- Next step — one verb ("Rename and retry", "Paste a Sheets URL")
function userError(message, nextStep) {
var text = String(message || 'Something went wrong');
if (nextStep) text += '\n\nNext: ' + nextStep;
return new Error(text);
}
// throw userError(
// 'Row 14 is missing Workbook URL.',
// 'Paste a Google Sheets link in column Workbook URL, then run New client again.'
// );
Keep code details in Logger / Log sheet, not in the modal.
How should menu actions talk to people?
Menu clicks have a UI — use it. Catch at the edge; translate before alert.
function menuNewClientSheet() {
var ui = SpreadsheetApp.getUi();
try {
var name = promptClientName_(ui);
createClientSheet_(name);
SpreadsheetApp.getActiveSpreadsheet().toast('Created “‘ + name + '”', 'Clients', 5);
} catch (err) {
logRow_('ERROR', 'menuNewClientSheet', err.message || String(err));
ui.alert(
'Couldn’t create the client sheet',
humanize_(err),
ui.ButtonSet.OK
);
}
}
function humanize_(err) {
var msg = (err && err.message) ? err.message : String(err);
// Map known technical patterns → operator language
if (/already exists/i.test(msg)) {
return msg + '\n\nNext: Pick a different tab name (or delete/rename the old tab).';
}
if (/missing template/i.test(msg)) {
return 'The "Client Template" tab is missing or renamed.\n\nNext: Restore that tab (or tell Joe) before creating clients.';
}
if (/DRIVE|openById|Invalid argument/i.test(msg)) {
return 'That Workbook URL/id didn’t open.\n\nNext: Paste a full Sheets link from the browser address bar.';
}
// Fall back to the message if you already threw userError(...)
return msg;
}
How do you validate before you write?
Half-finished rows are worse than a loud refusal. Check headers and required cells first.
function assertHeaders_(sheet, required) {
var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0]
.map(function (h) { return String(h == null ? '' : h).trim(); });
var missing = required.filter(function (name) {
return headers.indexOf(name) === -1;
});
if (missing.length) {
throw userError(
'Sheet "' + sheet.getName() + '" is missing columns: ' + missing.join(', ') + '.',
'Add those header names on row 1 (exact spelling), then retry.'
);
}
}
function assertRowHas_(rowObj, fields) {
fields.forEach(function (field) {
var v = rowObj[field];
if (v === '' || v == null) {
throw userError(
'Row ' + rowObj._row + ' is missing "' + field + '".',
'Fill "' + field + '" on that row, then run again.'
);
}
});
}
Parse Drive links with a helper that returns '' on miss, then throw a row-aware user error — don't let openById('') speak first.
function workbookIdFromRow_(rowObj) {
var id = extractDriveFileId(rowObj['Workbook URL']);
if (!id) {
throw userError(
'Row ' + rowObj._row + ' has a Workbook URL I can’t read.',
'Paste a Google Sheets link or file id, then retry.'
);
}
return id;
}
What about time-driven jobs with no UI?
Toasts are useless at 2am. Pattern:
- Log sheet line (
ERROR, function, message, row id) - Email yourself (or a shared ops inbox) with the same human sentence
- Optionally write
lastErroronto the job row so the sheet itself explains the failure
function nightlySync() {
try {
syncAll_();
} catch (err) {
var human = humanize_(err);
logRow_('ERROR', 'nightlySync', human);
emailErrorAlert('nightlySync needs attention', err, {
human: human.substring(0, 300),
spreadsheetId: SpreadsheetApp.getActiveSpreadsheet().getId()
});
throw err;
}
}
Put the actionable sentence in the email subject or first line so mobile preview helps.
How technical should Log sheet rows be?
Two channels:
| Channel | Audience | Content |
|---|---|---|
Modal / toast / job lastError |
Operators | Human sentence + next step |
| Log sheet / email stack snippet | You | Function, message, short stack, ids |
Don't make operators scroll stack frames to find "row 14."
Minimal test plan
- Missing required column → alert names the column and the fix
- Bad Drive URL → no raw
openByIdexception in the modal - Duplicate tab name → "pick another name" next step
- Trigger failure → email subject searchable; body leads with human sentence
- Happy path still toasts success in plain language
Soft Co-Pilot note
Translating throw sites into operator voice is tedious across a large script. NitroGAS has emailErrorAlert and related snippets; Co-Pilot can help draft humanize_ mappings for your column names. Free extension; Co-Pilot optional.
Closing checklist
- Menu entry points catch and
alerthumanized errors - Early asserts for headers / required fields / ids
- Job rows carry
lastErrorhumans can read - Nightly path logs + emails actionable text
- Stack traces stay in engineer channels
- Success toasts say what was created/updated
Happy Coding!


