cjpais/Handy · error
failed to create cancel item
Error message
failed to create cancel item
What it means
When the tray state is Recording or Transcribing, the rebuild first creates a cancel item before assembling the menu. This .expect panics if item creation fails. Notably this code path executes in the middle of an active recording/transcription session (the state change itself triggers the rebuild), so a panic here does not just crash the UI — it aborts a session where the user is actively dictating, losing the buffer unless cancellation cleanup runs.
Solutions
- Reproduce with a working tray present to confirm the environment is the cause
- Convert the Recording/Transcribing branch to construct the cancel item with graceful fallback (skip the item or reuse the idle layout) instead of .expect
- Debounce/route all update_tray_menu calls through a single serialized task so overlapping rebuilds cannot race
- On Linux, detect StatusNotifierWatcher availability at startup and degrade to no-tray mode instead of panicking mid-recording
Example fix
// before
let cancel_i = MenuItem::with_id(app, "cancel", &strings.cancel, true, None::<&str>)
.expect("failed to create cancel item");
// after
let Ok(cancel_i) = MenuItem::with_id(app, "cancel", &strings.cancel, true, None::<&str>) else {
error!("Failed to create cancel item; keeping previous tray menu");
return;
}; Defensive patterns
Strategy: fallback
Try / catch
let Ok(cancel_i) = MenuItem::with_id(app, "cancel", &strings.cancel, true, None::<&str>) else {
error!("cancel item failed; keeping previous menu mid-recording");
return; // never panic while the user is dictating
}; Prevention
- Treat recording-state code paths as crash-critical: any panic there loses the user's in-flight audio
- Build the cancel-bearing menu lazily and only when the recording state is stable, not on transient states
- Add integration tests that toggle recording on a tray-less Linux session to prove graceful degradation
When it happens
Trigger: Starting or finishing a recording on a Linux system whose DBusMenu registrar is missing/flaky; the state transition to Recording firing update_tray_menu at the exact moment the session tray restarts; rapid toggle of the global shortcut causing back-to-back rebuilds.
Common situations: Minimal window managers; CI smoke tests that toggle transcription without a desktop; users whose tray applet crashed mid-session.
Related errors
- failed to create menu
- 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/97f993d722eb9fd9.
Report an issue: GitHub.
Appendix: source
Thrown at src-tauri/src/tray.rs:286
let needs_icon = !state.lock().icons.contains_key(desired.icon_path);
let loaded_icon = if needs_icon {
match load_tray_icon(
app.path()
.resolve(desired.icon_path, tauri::path::BaseDirectory::Resource),
) {
Ok(image) => Some(image),
Err(err) => {
error!("Failed to load tray icon '{}': {err}", desired.icon_path);
None
}
}
} else {
None
};
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)
};
View on GitHub (pinned to fbd4e15fa1)