facebook/relay · error

Cannot have @module selections at multiple paths unless the

Error message

Cannot have @module selections at multiple paths unless the selections are within fields.

What it means

The @match/@module validation rejects documents where @module selections appear at multiple paths without being nested inside fields. When a duplicate module key is found at another path but the parent is not a field (parent_name is None — e.g. the selection sits directly at a fragment root), the code panics on parent_name.expect(...) instead of emitting the InvalidModuleSelectionWithoutKey diagnostic.

Source

Thrown at compiler/crates/relay-transforms/src/match_/match_transform.rs:428

                    .collect::<Vec<&str>>()
                    .join("_");
                match_directive_key_argument =
                    format!("{match_directive_key_argument}_{alias_path_str}").intern();
            }

            // If this is the first time we are encountering @module at this path, also ensure
            // that we have not previously encountered another @module associated with the same
            // match_directive_key_argument.
            //
            // This ensures that all of the @module's associated with a given @match occur at
            // a single path.
            let matches = match self.matches_for_path.get_mut(&self.path) {
                None => {
                    let existing_match_with_key = self.matches_for_path.values().any(|entry| {
                        entry.match_directive_key_argument == match_directive_key_argument
                    });
                    if existing_match_with_key {
                        let parent_name = parent_name.expect("Cannot have @module selections at multiple paths unless the selections are within fields.");
                        return Err(Diagnostic::error(
                            ValidationMessage::InvalidModuleSelectionWithoutKey {
                                document_name: self.document_name,
                                parent_name: parent_name.item,
                            },
                            parent_name.location,
                        ));
                    }
                    self.matches_for_path.insert(
                        self.path.clone(),
                        Matches {
                            match_directive_key_argument,
                            types: Default::default(),
                        },
                    );
                    self.matches_for_path.get_mut(&self.path).unwrap()
                }
                Some(matches) => matches,

View on GitHub (pinned to 668b1b85e0)

Solutions

  1. Nest each @module inline fragment under a concrete field so every selection has a distinct path and a parent field name.
  2. De-duplicate module keys so a given key argument appears at only one path, or give each path a distinct key argument.
  3. Rewrite the fragment so module selections are never directly at the fragment root; wrap them in selections on fields.
  4. If parallel module selections are intended, split them into separate fragments.
  5. If a seemingly valid document hits this, minimize it and file an issue — this branch should produce a diagnostic, not a panic.

Example fix

// before
fragment F on Query {
  ... on Post @module(name: "PostBody") { body }
  ... on Story @module(name: "PostBody") { body }
}
// after
fragment F on Query {
  post { ... on Post @module(name: "PostBody") { body } }
  story { ... on Story @module(name: "PostBodyStory") { body } }
}
Defensive patterns

Strategy: validation

Validate before calling

function validateModuleSelectionPaths(fragment) {
  const seen = new Map(); // key argument -> path
  for (const sel of flattenSelections(fragment.selectionSet)) {
    if (sel.kind !== 'InlineFragment' || !hasModuleDirective(sel)) continue;
    const key = getModuleKeyArg(sel);
    const underField = isUnderFieldSelection(sel);
    if (seen.has(key) || !underField) {
      return { ok: false, key, underField };
    }
    seen.set(key, pathOf(sel));
  }
  return { ok: true };
}
// before compiling: const r = validateModuleSelectionPaths(fragment); if (!r.ok) throw ...

Type guard

function isModuleSelectionWithinField(node) {
  let p = node.parent;
  while (p) {
    if (p.kind === 'Field') return true;
    if (p.kind === 'Fragment' || p.kind === 'Operation') return false;
    p = p.parent;
  }
  return false;
}

Try / catch

try {
  program = applyMatchTransform(program);
} catch (e) {
  if (String(e.message).includes('@module selections at multiple paths')) {
    reportDocumentError(documentName, 'Place @module selections inside fields with unique key arguments');
  }
  throw e;
}

Prevention

When it happens

Trigger: Placing @module inline fragments/spreads directly at a fragment's top level (not under a concrete field) such that the same module key argument appears at more than one path: matches_for_path already holds an entry with that match_directive_key_argument and parent_name is None.

Common situations: Authoring a fragment with two @module fragments at sibling top-level positions; a refactor moved a @module fragment out from under its parent field; programmatically generated fragments lacking a field wrapper; assuming @module fragments can float at fragment root.

Related errors


AI-assisted analysis of facebook/relay@668b1b85e0 (2026-09-02). Data as JSON: /api/errors/3bcc2547815a7ff1. Report an issue: GitHub.