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
- Nest each @module inline fragment under a concrete field so every selection has a distinct path and a parent field name.
- De-duplicate module keys so a given key argument appears at only one path, or give each path a distinct key argument.
- Rewrite the fragment so module selections are never directly at the fragment root; wrap them in selections on fields.
- If parallel module selections are intended, split them into separate fragments.
- 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
- Always nest @module fragments under a concrete field, never directly at fragment root.
- Give each @module selection path a unique key argument.
- Lint fragments for @module directives not preceded by a Field selection.
- Split intentionally parallel module selections into separate fragments.
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
- expected to find a supported argument as checked before
- Expect the module import inline fragment to have a type
- unexpected value for @defer if argument: {other:?}
- unexpected value for @stream if argument: {other:?}
- Unexpected custom directives: {:#?}
AI-assisted analysis of facebook/relay@668b1b85e0 (2026-09-02).
Data as JSON: /api/errors/3bcc2547815a7ff1.
Report an issue: GitHub.