facebook/relay · error

Expected field to be defined on concrete type

Error message

Expected field to be defined on concrete type

What it means

This panic fires while collecting fragment dependencies of a @relay_resolver defined on an interface. For each object implementing the interface, the transform looks up the interface's field on the concrete object; if any implementing object lacks that field, the lookup returns None and the transform panics. Relay requires every implementing object to define the field so dependency fragments can be gathered per concrete type.

Source

Thrown at compiler/crates/relay-transforms/src/relay_resolvers/fragment_dependencies.rs:109

        })
        .and_then(|arg| arg.value.get_string_literal().map(FragmentDefinitionName));

    if let Some(name) = name {
        return vec![name];
    }

    let mut fragment_names = Vec::new();
    // For a resolver field on an abstract type, we currently need to include rootFragments for all implementations,
    // because compiling resolvers don't have incremental mode
    if let Some(Type::Interface(interface_type)) = field.parent_type {
        let interface = schema.interface(interface_type);
        let implementing_objects = interface.recursively_implementing_objects(schema);

        // Collect all fragment names from concrete implementations
        for object_id in implementing_objects.iter() {
            let concrete_field_id = schema
                .named_field(Type::Object(*object_id), field.name.item)
                .expect("Expected field to be defined on concrete type");
            let concrete_field = schema.field(concrete_field_id);

            if let Some(fragment_name) = concrete_field
                .directives
                .named(*RELAY_RESOLVER_DIRECTIVE_NAME)
                .filter(|resolver_directive| {
                    let generated = resolver_directive
                        .arguments
                        .named(*GENERATED_FRAGMENT_ARGUMENT_NAME)
                        .and_then(|arg| arg.value.get_bool_literal())
                        .unwrap_or(false);
                    !generated
                })
                .and_then(|resolver_directive| {
                    resolver_directive
                        .arguments
                        .named(*FRAGMENT_KEY_ARGUMENT_NAME)
                })

View on GitHub (pinned to 668b1b85e0)

Solutions

  1. Add the missing @relay_resolver field to every type that implements the interface.
  2. Run graphql validation (fragment spread/type refinement validation) before the transform so mismatched interface fields are caught earlier.
  3. Regenerate concrete types from the updated interface definitions if types are generated.
  4. Temporarily narrow the interface (remove non-conforming implementors) until all implementors define the field.

Example fix

// before
interface Node { image_url: String @relay_resolver(import_path: "...", fragment_name: "...") }
// after
type User implements Node { image_url: String @relay_resolver(...) } // every implementor defines the field
Defensive patterns

Strategy: validation

Validate before calling

// Before compiling, check every implementor defines the interface's resolver field:
for object_id in interface.recursively_implementing_objects(schema).iter() {
    assert!(
        schema.named_field(Type::Object(*object_id), field.name.item).is_some(),
        "type {} does not define resolver field {}",
        schema.object(*object_id).name.item.0, field.name.item.0
    );
}

Type guard

fn interface_field_defined_on_all_implementors(schema: &SDLSchema, interface: &Interface, field_name: &str) -> bool {
    interface.recursively_implementing_objects(schema).iter().all(|id| {
        schema.named_field(Type::Object(*id), field_name).is_some()
    })
}

Try / catch

// Panics abort the compiler process; validate ahead instead. If embedding the compiler, isolate it:
let output = std::panic::catch_unwind(|| run_relay_compiler(config));
match output {
    Ok(result) => result,
    Err(_) => Err("compiler panicked: interface resolver field missing on an implementor"),
}

Prevention

When it happens

Trigger: An interface (or object spreading an interface fragment) declares a @relay_resolver field, but one or more objects implementing that interface do not define that field in their own selection set / type definition — detected during visit_selections or get_ir_definition_references.

Common situations: Adding a @relay_resolver field to an interface and forgetting to add it to some implementing type; a new object type implementing the interface without updating its fields; codegen regenerating types after the interface changed.

Related errors


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