facebook/relay · error
shadow resolver with @returnFragment must declare a @rootFra
Error message
shadow resolver with @returnFragment must declare a @rootFragment
What it means
In shadow_transplant_selection, a shadow resolver carrying @returnFragment must have a @rootFragment-derived fragment_name in its metadata. The code explicitly documents that a missing root fragment here is a compiler bug, not a user error, and panics so the failure is loud instead of silently dropping the consumer's selections from the main operation.
Source
Thrown at compiler/crates/relay-transforms/src/relay_resolvers/spread_transform.rs:277
// where the resolver's returned pointer targets the same record the
// shadowed field navigates to -- entirely from the store, with no network
// roundtrip. When the pointer instead targets a different server object,
// that record's selections are absent from the store and the runtime's
// client-edge availability check fires the generated `ClientEdgeQuery`
// refetch. The two arms are complementary: the transplant populates the
// common case, the refetch backstops the cross-object case, and the
// runtime selects between them per read based on what is in the store.
// A magic fragment (one declaring a `@returnFragment`) is required to
// also declare a `@rootFragment` (enforced by the
// `ReturnFragmentRequiresRootFragment` validation), and that root fragment
// must be present in the program by the time the spread transform runs. A
// missing root fragment here is therefore a compiler bug, not a user
// error -- fail loudly rather than silently skipping the transplant
// (which would drop the consumer's selections from the main operation).
let root_fragment_name = field_metadata
.fragment_name
.expect("shadow resolver with @returnFragment must declare a @rootFragment");
let root_fragment = self.program.fragment(root_fragment_name).unwrap_or_else(|| {
panic!("shadow resolver root fragment `{root_fragment_name}` must be present in the program")
});
// Clone the root-fragment path from the root down to the shadowed field
// (the one carrying the `ShadowReturnMarker` for this resolver's return
// fragment), preserving every ancestor linked field / inline fragment /
// condition along with its directives and arguments. At the marked field
// we splice in the consumer's selections (re-bound onto the shadowed
// server type). Returns `None` (no marker found) only on an unvalidated
// magic-fragment edge, which earlier validation passes prevent.
// Build a substitution from the root fragment's local argument variables
// (its `@argumentDefinitions`) to the values the consumer passed at the
// resolver field call site. The transplanted path is spliced directly into
// the consumer operation, so a reference to a root-fragment argument
// variable (e.g. `nodes(ids: $ids)`) must be remapped to the consumer's
// value (e.g. `$mixed_campaign_group_ids`); otherwise the operation would
// reference an undefined variable. For the normal (non-transplanted)View on GitHub (pinned to 668b1b85e0)
Solutions
- Ensure the pass that resolves @returnFragment into @rootFragment metadata runs before the spread transform.
- Compile the whole program together so the root fragment definition exists in the program (the companion panic covers the missing-definition case).
- Capture the failing document and file a relay compiler bug — the comment says this is never a user error.
- Bisect relay compiler versions to find the regression.
Defensive patterns
Strategy: validation
Validate before calling
assert!(field_metadata.fragment_name.is_some(), "@returnFragment resolver `{}` must declare @rootFragment", field_metadata.name); Type guard
fn has_root_fragment(m: &FieldMetadata) -> bool { m.fragment_name.is_some() } Try / catch
let Some(root_fragment_name) = field_metadata.fragment_name else {
return Err(Diagnostic::error("@returnFragment resolver missing @rootFragment"));
}; Prevention
- Keep @returnFragment lowering passes before the spread transform.
- Bisect relay versions when this appears after an upgrade.
- Treat this panic as a compiler bug and file a repro.
When it happens
Trigger: A shadowed field's resolver metadata lacks fragment_name when the spread transform transplants the shadow fragment's selections into the main operation — i.e. the @returnFragment lowering did not produce a root fragment entry.
Common situations: Hand-edited or partially applied transform pipeline; a Relay version mismatch between the codegen/runtime and the compiler crates; a compiler regression around @returnFragment.
Related errors
- Expected to have a fragment name.
- Expect to have a fragment node.
- useRefetchableFragmentNode: Unexpected action type
- commitMutation: Expected mutation operation
- commitMutation: Expected mutation operation
AI-assisted analysis of facebook/relay@668b1b85e0 (2026-09-02).
Data as JSON: /api/errors/8215cb591084468d.
Report an issue: GitHub.