{"record":{"id":"9f3e68c7d2f65fa2","repo":"cjpais/Handy","slug":"failed-to-create-quit-item","errorCode":null,"errorMessage":"failed to create quit item","messagePattern":"failed to create quit item","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src-tauri/src/tray.rs","lineNumber":241,"sourceCode":"/// (theme, Secure Input warning). The recording state itself is preserved.\npub fn refresh_tray_icon(app: &AppHandle) {\n    sync_tray(app);\n}\n\n/// Re-syncs the tray after something the menu depends on changed (model\n/// list/selection/loaded state, language, settings).\npub fn update_tray_menu(app: &AppHandle) {\n    sync_tray(app);\n}\n\n/// Records the current desired tray state and schedules one apply on the main\n/// thread (or lets an already-pending apply pick it up). Never blocks on the\n/// main thread.\n///\n/// The snapshot (settings, model list, loaded state) is computed on the\n/// *calling* thread on purpose: the main-thread applier must not take manager\n/// locks that a worker may hold across slow work (see #1716).\npub fn sync_tray(app: &AppHandle) {\n    sync_tray_with(app, |_| {});\n}\n\nfn sync_tray_with(app: &AppHandle, update: impl FnOnce(&mut TrayInner)) {\n    let Some(state) = app.try_state::<TrayState>() else {\n        return;\n    };\n\n    // Record intent and claim a sequence number in one critical section, so\n    // sequence order == the order in which state changes were requested.\n    let (seq, icon_state) = {\n        let mut inner = state.lock();\n        update(&mut inner);\n        inner.next_seq += 1;\n        (inner.next_seq, inner.icon_state)\n    };\n\n    // Tray not built yet (early secure-input monitor callbacks). The intent","sourceCodeStart":223,"sourceCodeEnd":259,"githubUrl":"https://github.com/cjpais/Handy/blob/fbd4e15fa14a721c66c57006ae110428b9e255b3/src-tauri/src/tray.rs#L223-L259","documentation":"update_tray_menu() rebuilds the whole tray context menu on every tray-state or language change using Tauri's tray API backed by the muda menu library. MenuItem::with_id registers a native menu item with the app's menu manager; this .expect panics when that registration returns Err. On Linux the tray/menu path goes through DBusMenu, so it fails when no StatusNotifierWatcher/registrar is reachable; on other platforms it is nearly always a shutdown race where the event loop or app handle is being torn down.","triggerScenarios":"Calling update_tray_menu on Linux under GNOME (no tray extension), sway/hyprland without a waybar-like StatusNotifierWatcher, or a headless/CI session; a tray-state change (Idle->Recording) or language change firing while the app is exiting, so MenuItem::with_id(app, \"quit\", ...) hits a dead menu manager.","commonSituations":"Testing Handy on minimal Wayland compositors or GNOME without an AppIndicator extension; systemd services or containers that auto-start the app without a session bus; races during app quit when a final update_tray_menu sneaks in after teardown.","solutions":["Reproduce the environment first: on Linux run a tray provider (KDE Plasma, waybar with tray, GNOME AppIndicator extension) and confirm the panic disappears","Convert update_tray_menu to return Result and log-and-keep the previous menu instead of .expect, so one failed rebuild cannot take down the app","Guard rebuilds during shutdown: skip update_tray_menu once an exit flag is set, and debounce state-change rebuilds","Keep tauri and muda versions in lockstep when upgrading, since the menu-manager API and its error surface change between releases"],"exampleFix":"// before\nlet quit_i = MenuItem::with_id(app, \"quit\", &strings.quit, true, quit_accelerator)\n    .expect(\"failed to create quit item\");\n\n// after\nlet quit_i = match MenuItem::with_id(app, \"quit\", &strings.quit, true, quit_accelerator) {\n    Ok(item) => item,\n    Err(e) => {\n        error!(\"Failed to create quit item, keeping previous tray menu: {e}\");\n        return;\n    }\n};","handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"// In update_tray_menu, treat any item-creation Err as 'abort rebuild, keep previous menu':\nlet quit_i = match MenuItem::with_id(app, \"quit\", &strings.quit, true, quit_accelerator) {\n    Ok(i) => i,\n    Err(e) => { error!(\"tray rebuild failed (quit item): {e}\"); return; }\n};","preventionTips":["Never .expect on muda/Tauri menu constructors in code that runs on every state change — return Result and keep the previous menu on failure","On Linux, verify a StatusNotifierWatcher exists (qdbus org.kde.StatusNotifierWatcher) before relying on tray code in tests","Serialize tray rebuilds through one task/queue so overlapping state changes cannot interleave","Skip rebuilds once a shutdown flag is set"],"tags":["tauri","tray","muda","rust","panic","linux"],"backgroundTag":"tauri-tray-menu-creation-failed","analyzedSha":"fbd4e15fa14a721c66c57006ae110428b9e255b3","analyzedAt":"2026-08-17T10:29:55.597Z","contentChangedAt":"2026-08-17T10:29:55.597Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}