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

  1. Ensure the pass that resolves @returnFragment into @rootFragment metadata runs before the spread transform.
  2. Compile the whole program together so the root fragment definition exists in the program (the companion panic covers the missing-definition case).
  3. Capture the failing document and file a relay compiler bug — the comment says this is never a user error.
  4. 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

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


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