temporalio/temporal · error

component type %s must be struct or pointer to struct

Error message

component type %s must be struct or pointer to struct

What it means

chasm.Registry.registerComponent rejects component implementations whose reflect.Type is not a struct or a pointer to a struct. The Component interface is backed by Go structs, so registering the interface type itself or any other kind (map, slice, int, etc.) cannot be managed by the framework. This guard exists to prevent accidentally registering the interface type when the intent was a concrete implementation.

Source

Thrown at chasm/registry.go:273

	if existingComponent, ok := r.rcByID[id]; ok {
		return fmt.Errorf("component ID %d collision between %s and %s", id, fqn, existingComponent.fqType())
	}

	for key, value := range rc.contextValues {
		if existingValue, ok := r.rcContextValues[key]; ok {
			return fmt.Errorf("context value key %v registered by component %s conflicts with component %s", key, fqn, existingValue.fqn)
		}
		r.rcContextValues[key] = valueWithFqn{
			v:   value,
			fqn: fqn,
		}
	}

	// rc.goType implements Component interface; therefore, it must be a struct.
	// This check to protect against the interface itself being registered.
	if !(rc.goType.Kind() == reflect.Struct ||
		(rc.goType.Kind() == reflect.Pointer && rc.goType.Elem().Kind() == reflect.Struct)) {
		return fmt.Errorf("component type %s must be struct or pointer to struct", rc.goType.String())
	}
	if _, ok := r.rcByGoType[rc.goType]; ok {
		return fmt.Errorf("component type %s is already registered", rc.goType.String())
	}
	r.warnUnmanagedFields(fqn, rc)

	r.rcByFqn[fqn] = rc
	r.rcByID[id] = rc
	r.rcByGoType[rc.goType] = rc
	return nil
}

func (r *Registry) validate(rc *RegistrableComponent) error {
	if err := r.validateName(rc.componentType); err != nil {
		return err
	}
	return r.validateVisibilityBusinessIDAlias(rc)
}

View on GitHub (pinned to bde624efd1)

Solutions

  1. Register the concrete struct type: chasm.Register[MyComponentImpl](registry, ...).
  2. If *MyComponentImpl is used elsewhere, either kind works — but never the interface type.
  3. Print reflect.TypeOf to confirm the registered type is a struct or *struct.

Example fix

// before
chasm.Register[OrderComponent](reg, opts)
// after
chasm.Register[OrderComponentImpl](reg, opts)
Defensive patterns

Strategy: validation

Validate before calling

t := reflect.TypeOf((*MyComponent)(nil)).Elem()
if t.Kind() == reflect.Interface ||
	!(t.Kind() == reflect.Struct || (t.Kind() == reflect.Pointer && t.Elem().Kind() == reflect.Struct)) {
	return fmt.Errorf("%s must be struct or pointer to struct", t)
}

Type guard

func isStructOrStructPtr(t reflect.Type) bool {
	return t != nil && (t.Kind() == reflect.Struct || (t.Kind() == reflect.Pointer && t.Elem().Kind() == reflect.Struct))
}

Prevention

When it happens

Trigger: Calling chasm.Register (which calls registerComponent) with a generic parameter that is an interface type, e.g. Register[SomeComponent](...), or a non-struct type such as a named map/slice/func type.

Common situations: Developer passes the interface name instead of the concrete struct; refactor renamed a struct into an interface; copy-pasted registration code where the type parameter was swapped; registering a type alias pointing to a non-struct kind.

Related errors


AI-assisted analysis of temporalio/temporal@bde624efd1 (2026-09-01). Data as JSON: /api/errors/bb83f3fa922c28fe. Report an issue: GitHub.