cjpais/Handy · warning

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

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.

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)

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.