{"record":{"id":"42e5aa26649b3ce4","repo":"cjpais/Handy","slug":"failed-to-update-binding","errorCode":null,"errorMessage":"Failed to update binding","messagePattern":"Failed to update binding","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/stores/settingsStore.ts","lineNumber":389,"sourceCode":"                  [id]: {\n                    ...state.settings.bindings?.[id]!,\n                    current_binding: binding,\n                  },\n                },\n              }\n            : null,\n        }));\n\n        const result = await commands.changeBinding(id, binding);\n\n        // Check if the command executed successfully\n        if (result.status === \"error\") {\n          throw new Error(result.error);\n        }\n\n        // Check if the binding change was successful\n        if (!result.data.success) {\n          throw new Error(result.data.error || \"Failed to update binding\");\n        }\n      } catch (error) {\n        console.error(`Failed to update binding ${id}:`, error);\n\n        // Rollback on error\n        if (originalBinding && get().settings) {\n          set((state) => ({\n            settings: state.settings\n              ? {\n                  ...state.settings,\n                  bindings: {\n                    ...state.settings.bindings,\n                    [id]: {\n                      ...state.settings.bindings?.[id]!,\n                      current_binding: originalBinding,\n                    },\n                  },\n                }","sourceCodeStart":371,"sourceCodeEnd":407,"githubUrl":"https://github.com/cjpais/Handy/blob/fbd4e15fa14a721c66c57006ae110428b9e255b3/src/stores/settingsStore.ts#L371-L407","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nif (!result.data.success) {\n  throw new Error(result.data.error || \"Failed to update binding\");\n}\n\n// after — always carry actionable context even when the backend omits data.error\nif (!result.data.success) {\n  throw new Error(\n    result.data.error ?? `Backend rejected binding ${id}=${binding}: success=false with no error detail`\n  );\n}","handlingStrategy":"try-catch","validationCode":"// reject ids the backend doesn't know and combos already claimed by another binding\nconst bindings = get().settings?.bindings ?? {};\nif (!(id in bindings) && !DEFAULT_BINDING_IDS.includes(id)) {\n  throw new Error(`Unknown binding id: ${id}`);\n}\nconst comboInUse = Object.entries(bindings).find(\n  ([otherId, b]) => otherId !== id && b.current_binding === binding\n);\nif (comboInUse) {\n  throw new Error(`Combo already used by: ${comboInUse[0]}`);\n}","typeGuard":"export function isBindingRejected(\n  r: Result<BindingResponse, string>\n): r is { status: \"ok\"; data: BindingResponse } {\n  return r.status === \"ok\" && r.data.success === false;\n}","tryCatchPattern":"try {\n  const result = await commands.changeBinding(id, binding);\n  if (result.status === \"ok\" && !result.data.success) {\n    // prefer the backend's reason; only fall back to generic text when error is null\n    throw new Error(result.data.error ?? `Backend rejected binding ${id}`);\n  }\n} catch (error) {\n  // rollback (already implemented) + show the specific reason to the user\n}","preventionTips":["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"],"tags":["tauri","keyboard-shortcut","bindings","registration","typescript"],"backgroundTag":"shortcut-registration-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"}