cjpais/Handy · error · Error
Failed to update binding
Error message
Failed to update binding
What it means
The changeBinding command executed (result.status === "ok") but the backend returned a BindingResponse with success:false — the handler refused to apply the new binding and put the reason in data.error (typed `string | null`). The generic "Failed to update binding" text only appears when the backend left error null; known success:false paths include an id that exists neither in settings nor in defaults (shortcut/mod.rs:140), and shortcut registration/unregistration failures.
Solutions
- Inspect result.data.error — it usually names the real cause; surface it in the UI instead of the generic fallback text
- Verify the id passed is a real shortcut id from settings.bindings or the defaults map
- Pick a key combo not already used by another binding or a system/global shortcut of another app
- On macOS grant Accessibility (and Input Monitoring) permission to the app, then retry
- Retry after the automatic rollback restores the original binding
Example fix
// before
if (!result.data.success) {
throw new Error(result.data.error || "Failed to update binding");
}
// after — always carry actionable context even when the backend omits data.error
if (!result.data.success) {
throw new Error(
result.data.error ?? `Backend rejected binding ${id}=${binding}: success=false with no error detail`
);
} Defensive patterns
Strategy: try-catch
Validate before calling
// reject ids the backend doesn't know and combos already claimed by another binding
const bindings = get().settings?.bindings ?? {};
if (!(id in bindings) && !DEFAULT_BINDING_IDS.includes(id)) {
throw new Error(`Unknown binding id: ${id}`);
}
const comboInUse = Object.entries(bindings).find(
([otherId, b]) => otherId !== id && b.current_binding === binding
);
if (comboInUse) {
throw new Error(`Combo already used by: ${comboInUse[0]}`);
} Type guard
export function isBindingRejected(
r: Result<BindingResponse, string>
): r is { status: "ok"; data: BindingResponse } {
return r.status === "ok" && r.data.success === false;
} Try / catch
try {
const result = await commands.changeBinding(id, binding);
if (result.status === "ok" && !result.data.success) {
// prefer the backend's reason; only fall back to generic text when error is null
throw new Error(result.data.error ?? `Backend rejected binding ${id}`);
}
} catch (error) {
// rollback (already implemented) + show the specific reason to the user
} Prevention
- Check the chosen combo against all other current_binding values before calling changeBinding
- Keep shortcut ids centralized (shared constants) so frontend never sends unknown ids
- On macOS, prompt for Accessibility/Input Monitoring permission before first shortcut change
- Backend: always populate BindingResponse.error when success is false so callers never see the generic fallback
When it happens
Trigger: Calling changeBinding with an unknown shortcut id (not in settings.bindings and not in get_default_settings()); the new combo failing OS-level registration in register_shortcut (device/permission problems or an already-registered accelerator); failing to unregister the previous binding before applying the new one.
Common situations: Typo'd or renamed shortcut id in frontend code; duplicate key combos claimed by another binding or another app; missing accessibility/input-monitoring permissions on macOS; rapidly editing the same binding from two windows.
Related errors
- result.error
- Device index ( ) cannot host a model
- error_msg (dynamic: model loading failure message)
- error while building tauri application
- Failed to check macOS permissions:
AI-assisted analysis of cjpais/Handy@fbd4e15fa1 (2026-08-17).
Data as JSON: /api/errors/42e5aa26649b3ce4.
Report an issue: GitHub.
Appendix: source
Thrown at src/stores/settingsStore.ts:389
[id]: {
...state.settings.bindings?.[id]!,
current_binding: binding,
},
},
}
: null,
}));
const result = await commands.changeBinding(id, binding);
// Check if the command executed successfully
if (result.status === "error") {
throw new Error(result.error);
}
// Check if the binding change was successful
if (!result.data.success) {
throw new Error(result.data.error || "Failed to update binding");
}
} catch (error) {
console.error(`Failed to update binding ${id}:`, error);
// Rollback on error
if (originalBinding && get().settings) {
set((state) => ({
settings: state.settings
? {
...state.settings,
bindings: {
...state.settings.bindings,
[id]: {
...state.settings.bindings?.[id]!,
current_binding: originalBinding,
},
},
}View on GitHub (pinned to fbd4e15fa1)