Someone renames exportReport to exportMonthlyReport, the menu still points at the old name, and every click says “Script function not found”. Or onOpen throws and the menu doesn't appear at all. installMenuSafely(caption, items) builds the menu inside try/catch, skips (and logs) any item whose function isn't defined, and never throws out of onOpen. Items are [label, fnName], or null for a separator.
When to use
- Every
onOpenthat builds a custom menu - Shared spreadsheets where several people edit the script
- Menus that grow over time and occasionally lose a function
How to use this snippet
/**
* Build a custom menu without letting a bad item break onOpen.
*
* @param {string} caption menu title
* @param {Array<Array<string>|null>} items [label, functionName] or null for a separator
* @return {{added: number, skipped: string[]}}
*/
function installMenuSafely(caption, items) {
var result = { added: 0, skipped: [] };
try {
var menu = SpreadsheetApp.getUi().createMenu(caption);
(items || []).forEach(function (item) {
if (item === null) { menu.addSeparator(); return; }
var label = item[0], fnName = item[1];
if (typeof globalThis[fnName] !== 'function') {
result.skipped.push(fnName);
console.warn('installMenuSafely: skipped "' + label + '", ' + fnName + ' is not defined');
return;
}
menu.addItem(label, fnName);
result.added++;
});
menu.addToUi();
} catch (err) {
// No UI (trigger or editor run) or a menu API error: log it, don't break onOpen
console.warn('installMenuSafely: menu not installed: ' + (err && err.message ? err.message : err));
}
return result;
}
Example
function onOpen() {
installMenuSafely('Reports', [
['Refresh data', 'refreshData'],
['Export monthly report', 'exportMonthlyReport'],
null,
['Settings…', 'showSettingsSidebar']
]);
}
Tips:
globalThisneeds the V8 runtime (the default for new projects). On the old Rhino runtime, switch to V8 in Project Settings.- Keep
onOpenthis small. Anything slow or that needs authorization belongs in the menu functions, not inonOpen. - Check the Executions log after a rename: a skipped item shows up there as a warning.
- Library functions aren't on
globalThis. Wrap them in a local function and put that in the menu.
Tip: NitroGAS Co-Pilot can convert an existing onOpen menu to installMenuSafely. Paste your onOpen.
Happy Coding!
