cjpais/Handy · error
failed to create menu
Error message
failed to create menu
What it means
After creating all children, the Recording/Transcribing branch assembles them with Menu::with_items(...).expect("failed to create menu"). This final assembly fails if the native backend rejects the menu object itself or any passed child is in an invalid state. Because it runs on the recording-state rebuild, a panic here kills an active dictation session — the worst-timed crash in the tray module.
Solutions
- Ensure a system tray exists when testing recording flows on Linux
- Wrap the whole update_tray_menu in a Result-returning helper: on Menu::with_items failure, log, keep tray.set_menu untouched, and leave the icon state unchanged
- Serialize rebuilds through one channel/task and drop rebuild requests that arrive during teardown
- Consider building the menu without accelerators as a fallback on repeated failures
Example fix
// before
Menu::with_items(app, &[&version_i, &separator(), &cancel_i, /* ... */])
.expect("failed to create menu")
// after
match Menu::with_items(app, &[&version_i, &separator(), &cancel_i, /* ... */]) {
Ok(menu) => menu,
Err(e) => {
error!("Failed to assemble recording tray menu, keeping previous: {e}");
return;
}
} Defensive patterns
Strategy: fallback
Try / catch
match Menu::with_items(app, &[&version_i, &separator(), &cancel_i, /* ... */]) {
Ok(menu) => menu,
Err(e) => { error!("recording menu assembly failed: {e}"); return; }
} Prevention
- Assemble menus from already-validated children and treat assembly failure as non-fatal
- Keep the previous TrayIcon menu handle alive until the new one is fully built so a failure can fall back
- Log the muda error kind; recurring backend failures should disable tray rebuilds for the session
When it happens
Trigger: Toggling recording on a Linux desktop without a functioning StatusNotifierWatcher/DBusMenu; passing the accelerator-bearing items (settings/quit carry Ctrl+Q style accelerators) to a backend that rejects them; rebuild racing app shutdown.
Common situations: Tray-less Wayland sessions; GNOME without extensions; automated tests driving state changes headlessly.
Related errors
- failed to create cancel item
- failed to create model item
- failed to create model submenu
- failed to create quit item
- failed to create separator
AI-assisted analysis of cjpais/Handy@fbd4e15fa1 (2026-08-17).
Data as JSON: /api/errors/b17ac3a653c91cd6.
Report an issue: GitHub.
Appendix: source
Thrown at src-tauri/src/tray.rs:302
let schedule = {
let mut inner = state.lock();
if let Some(image) = loaded_icon {
inner.icons.insert(desired.icon_path, image);
}
if seq < inner.desired_seq {
// A request triggered after this one already stored its snapshot
// (and scheduled an apply). Ours is stale; drop it.
trace!(
"tray sync: request {seq} superseded by {}",
inner.desired_seq
);
return;
}
inner.desired = Some(desired);
inner.desired_seq = seq;
// If an apply is already pending it will read the snapshot we just
// stored; otherwise schedule one.
!std::mem::replace(&mut inner.pending, true)
};
if schedule {
post_apply(app);
} else {
trace!("tray sync: apply already pending");
}
}
fn compute_desired(app: &AppHandle, icon_state: TrayIconState) -> TrayDesired {
let settings = settings::get_settings(app);
let theme = get_current_theme(app);
let warning = crate::secure_input::tray_warning_active(app);
let model_loaded = app.state::<Arc<TranscriptionManager>>().is_model_loaded();
let mut downloaded_models: Vec<(String, String)> = app
.state::<Arc<ModelManager>>()
.get_available_models()View on GitHub (pinned to fbd4e15fa1)