actualbudget/actual · error

Unhandled action type: ${action.type}

Error message

Unhandled action type: ${action.type}

What it means

updateFilterReducer handles only two action types: 'set-op' and 'set-value'. Its default branch throws this when dispatched with any other action type. The `@ts-expect-error` marker shows the action union is not enforced strictly here, so TypeScript cannot prevent invalid actions — making this a runtime-only guard. It protects the filter-conditions UI state machine from unknown messages.

Source

Thrown at packages/desktop-client/src/components/filters/updateFilterReducer.ts:54

        // Convert single value to array when switching to oneOf/notOneOf
        if (value === null || value === undefined) {
          value = [];
        } else if (!Array.isArray(value)) {
          // @ts-expect-error - fix me
          value = [value];
        }
      }
      return { ...state, op: action.op, value };
    }
    case 'set-value': {
      const { value } = makeValue(action.value, {
        type: FIELD_TYPES.get(state.field),
      });
      return { ...state, value };
    }
    default:
      // @ts-expect-error - fix me
      throw new Error(`Unhandled action type: ${action.type}`);
  }
}

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Log action.type in the error to identify the bogus dispatch.
  2. Correct the dispatch site to send only 'set-op' or 'set-value'.
  3. If a new behavior is needed (e.g. clearing a value), add an explicit case to updateFilterReducer and to the action union.
  4. Tighten the reducer's action parameter type so TypeScript rejects unknown action types (removing the need for @ts-expect-error).

Example fix

// before
dispatch({ type: 'set-field', field: 'date' } as any);
// after
dispatch({ type: 'set-op', op: 'is' });
dispatch({ type: 'set-value', value: '2024-01-01' });
Defensive patterns

Strategy: type-guard

Validate before calling

type FilterAction = { type: 'set-op'; op: RuleConditionEntity['op'] } | { type: 'set-value'; value: unknown };
if (action.type !== 'set-op' && action.type !== 'set-value') return state;

Type guard

function isFilterAction(a: unknown): a is FilterAction {
  return (
    typeof a === 'object' && a !== null &&
    ((a as FilterAction).type === 'set-op' || (a as FilterAction).type === 'set-value')
  );
}

Try / catch

try {
  dispatch(action);
} catch (err) {
  if (String(err).startsWith('Unhandled action type')) {
    logger.error('Invalid filter action dispatched', action);
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: Dispatching an action other than 'set-op'/'set-value' into a useReducer wired to updateFilterReducer — e.g. `{ type: 'set-field' }`, a typo like 'setop', or reusing this reducer for another filter feature with different action names.

Common situations: Copy-pasting dispatch calls from another filter reducer with a richer action set; renaming action types at one call site but not the dispatch site; extending the filter UI with new actions and forgetting to add reducer cases.

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/baacb422b6fa9dc5. Report an issue: GitHub.