Also comments by Gemini:

What are the issues of not awaiting asynchronous functions called from menu items in electron apps?

In Electron, application menus (Menu and MenuItem) are managed exclusively by the Main Process. When you assign an asynchronous click handler to a menu item, the underlying Electron framework triggers that callback synchronously.

If you do not explicitly handle or await asynchronous logic within those menu item actions (or fail to handle the floating promises), you run into several subtle but critical architecture, state, and stability bugs.

  1. Unhandled Promise Rejections (App Crashes)

The Electron menu item click property expects a standard synchronous callback function: click: (menuItem, browserWindow, event) => void.

If you pass an async function or call an async function inside it without a proper .catch() block or internal try/catch, it results in a floating promise. If that promise rejects (e.g., a file save dialog is canceled, a network call fails, or an IPC invocation throws an error), it triggers an Unhandled Promise Rejection. Depending on your configuration and Node version, this can silently swallow critical errors or abruptly crash the entire Main Process, shutting down your app.
JavaScript

// ❌ Dangerous: If performSave fails, the error goes unhandled.
const menuItem = new MenuItem({
label: 'Save',
click: async () => {
await performSave(); // No try/catch around this await!
}
});

  1. Race Conditions and UI State Desynchronization

When a user clicks a menu item, they expect the app's state to match the visual feedback. Because the menu callback doesn't block or wait for completion, it fires the async function and finishes instantly.

If the user clicks the menu item multiple times rapidly, it will fire multiple concurrent instances of your asynchronous task. This can lead to severe race conditions:

Overwriting files simultaneously.

Making redundant database mutations.

Corrupting application state if the second click finishes processing before the first one.
  1. Breaking App Graceful Shutdown (before-quit)

If a menu item triggers a long-running asynchronous operation (like "Export Database") and the user immediately hits Cmd+Q (macOS) or clicks "Quit" from the menu, Electron will attempt to close the application.

Because the menu action didn't flag that it was still "busy" (since it wasn't tracked or awaited by a lifecycle controller), Electron won't know it needs to wait. The Main process might terminate abruptly mid-operation, resulting in data corruption or partial file writes.

  1. UI Freeze via Main Process Blocking

A common misconception is that async/await inherently moves heavy execution to another thread. In Node.js, async code still runs on the single-threaded event loop of the Main process.

If your un-awaited menu callback kicks off heavy CPU-bound asynchronous tasks (like parsing massive JSON bundles, processing images, or synchronous crypto operations right after an await barrier), it will block the Main process event loop. This causes the entire application UI to stutter or freeze, stopping the Renderer process from handling window resizing, minimizing, or rendering animations.
Best Practices to Fix This

To avoid these pitfalls, follow these patterns when dealing with async logic in Electron menus:
A. Always Wrap in try/catch

Ensure that any error occurring inside the async chain is handled gracefully without crashing the main process.
JavaScript

const menuItem = new MenuItem({
label: 'Export Data',
click: async (menuItem, browserWindow) => {
try {
await exportDataLogic();
} catch (error) {
console.error('Export failed:', error);
// Safely alert the user via dialog
dialog.showErrorBox('Export Error', 'Failed to export your data.');
}
}
});

B. Implement a Debounce/Loading State

Prevent the user from triggering the action multiple times by dynamically disabling the menu item or the UI.
JavaScript

let isExporting = false;

const menuItem = new MenuItem({
label: 'Export Data',
click: async (menuItem) => {
if (isExporting) return;

try {
  isExporting = true;
  menuItem.enabled = false; // Disable to prevent multiple clicks

  await longRunningTask();
} catch (err) {
  // Handle error...
} finally {
  isExporting = false;
  menuItem.enabled = true; // Re-enable
}

}
});

C. Offload to the Renderer Process via IPC

Menus live in the Main process, but the state usually lives in the Renderer (the frontend). Instead of doing heavy processing in the menu click handler itself, use the click handler purely to emit an IPC message to your frontend, where you can easily manage loading spinners, UI disabling, and local error notifications.
JavaScript

// Main Process
const menuItem = new MenuItem({
label: 'Open Settings',
click: (menuItem, browserWindow) => {
if (browserWindow) {
// Simply signal the frontend and let it manage the async flow
browserWindow.webContents.send('menu-open-settings');
}
}
});