cjpais/Handy · warning

No handler for setting

Error message

No handler for setting: ${String(key)}

What it means

settingsStore.updateSetting optimistically patches local Zustand state, then looks up a per-key async updater in the settingUpdaters map (settingsStore.ts line 76) to persist the value through a Tauri command. Keys 'bindings' and 'selected_model' are excluded because they are persisted through dedicated flows. For any other key with no map entry this warning fires: the UI shows the new value immediately, but the backend never persists it, so the setting silently reverts on the next reload.

Solutions

  1. Add an updater entry to settingUpdaters mapping the key to its persist command (e.g. new_option: (v) => commands.changeNewOptionSetting(v as boolean))
  2. If the key is intentionally persisted elsewhere, add it to the exclusion condition alongside 'bindings' and 'selected_model' with a comment
  3. Make coverage compile-enforced: type settingUpdaters as Record<keyof Settings, updater | symbol> and fill gaps with an explicit UNHANDLED marker so missing keys fail typecheck
  4. Add a unit test enumerating Object.keys(defaultSettings) minus the exclusion list against Object.keys(settingUpdaters)

Example fix

// before: 'new_option' exists in Settings, toggle works, warning fires, value never persists
// after: src/stores/settingsStore.ts
const settingUpdaters: { [K in keyof Settings]?: (value: Settings[K]) => Promise<unknown> } = {
  // ...existing entries...
  new_option: (value) => commands.changeNewOptionSetting(value as boolean),
};
Defensive patterns

Strategy: type-guard

Validate before calling

// Unit test: every persisted Settings key must have an updater (or be excluded)
const EXCLUDED = new Set(["bindings", "selected_model"]);
const missing = (Object.keys(defaultSettings) as (keyof Settings)[])
  .filter((k) => !EXCLUDED.has(k) && !settingUpdaters[k]);
if (missing.length) throw new Error(`No handler for setting: ${missing.join(", ")}`);

Type guard

const hasUpdater = (key: keyof Settings): boolean =>
  key === "bindings" || key === "selected_model" || Boolean(settingUpdaters[key]);

Prevention

When it happens

Trigger: Adding a new field to the Settings/AppSettings type and wiring a toggle to updateSetting('newKey', v) without adding newKey: (v) => commands.changeNewKeySetting(v) to settingUpdaters; renaming a key in the type but not the map; a typo between the UI's key string and the map's key.

Common situations: Feature work adding a new preference; refactors that rename settings; review churn where the Rust command exists but the TS map entry was forgotten.

Related errors


AI-assisted analysis of cjpais/Handy@fbd4e15fa1 (2026-08-17). Data as JSON: /api/errors/77be41cc99fca270. Report an issue: GitHub.

Appendix: source

Thrown at src/stores/settingsStore.ts:332

      key: K,
      value: Settings[K],
    ) => {
      const { settings, setUpdating } = get();
      const updateKey = String(key);
      const originalValue = settings?.[key];

      setUpdating(updateKey, true);

      try {
        set((state) => ({
          settings: state.settings ? { ...state.settings, [key]: value } : null,
        }));

        const updater = settingUpdaters[key];
        if (updater) {
          await updater(value);
        } else if (key !== "bindings" && key !== "selected_model") {
          console.warn(`No handler for setting: ${String(key)}`);
        }
      } catch (error) {
        console.error(`Failed to update setting ${String(key)}:`, error);
        if (settings) {
          set({ settings: { ...settings, [key]: originalValue } });
        }
      } finally {
        setUpdating(updateKey, false);
      }
    },

    // Reset a setting to its default value
    resetSetting: async (key) => {
      const { defaultSettings } = get();
      if (defaultSettings) {
        const defaultValue = defaultSettings[key];
        if (defaultValue !== undefined) {
          await get().updateSetting(key, defaultValue as any);

View on GitHub (pinned to fbd4e15fa1)