Give non-technical teammates errors they can act on

TL;DR — Write Apps Script errors non-technical teammates can act on: plain toasts, named failures, row context, and alerts that say what to fix—not stack traces.

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 / toast for menu actions; email for triggers.
  • Never surface raw err.stack to operators (log it; don't modal it).
  • Validate early (headers, ids, required cells) before half-writing data.
  • One pattern: userMessage for humans, logRow + optional emailErrorAlert for 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:

  1. Outcome — what didn't happen ("Couldn't create the client tab")
  2. Pointer — row, sheet, field, or job id
  3. Likely cause — in operator vocabulary ("name already used", "link missing")
  4. 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:

  1. Log sheet line (ERROR, function, message, row id)
  2. Email yourself (or a shared ops inbox) with the same human sentence
  3. Optionally write lastError onto 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 openById exception 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 alert humanized errors
  • Early asserts for headers / required fields / ids
  • Job rows carry lastError humans can read
  • Nightly path logs + emails actionable text
  • Stack traces stay in engineer channels
  • Success toasts say what was created/updated

Happy Coding!