apache/beam · error

bad userfn

Error message

bad userfn

What it means

EncodeMultiEdge fails to serialize a DoFn into a protobuf Fn reference (encodeFn on (*graph.Fn)(edge.DoFn)) and wraps the cause as 'bad userfn'. Serialization of user functions requires the fn's package/type info to be resolvable (no closures, valid funcRef). This error surfaces during pipeline graph encoding, typically when submitting a job.

Solutions

  1. Read the wrapped inner error for the exact encodeFn failure (type not registered, unresolvable path)
  2. Replace closures/anonymous DoFns with named top-level types
  3. Ensure any types used as fn config fields are registered via gob/beam type registration
  4. Verify the package path is importable and matches what the runner expects

Example fix

// before
beam.ParDo(s, &struct{ process func(string) }{...}, in) // anonymous/unserializable
// after
type myFn struct{}
func (myFn) ProcessElement(s string) string { return s }
beam.ParDo(s, &myFn{}, in)
Defensive patterns

Strategy: validation

Validate before calling

if reflect.TypeOf(fn).Kind() == reflect.Struct && !isNamedTopLevel(fn) {
  return errors.New("DoFn must be a named top-level type, not a closure or anonymous struct")
}

Type guard

func isSerializableDoFn(fn interface{}) bool {
  t := reflect.TypeOf(fn)
  return t != nil && t.Name() != "" && t.PkgPath() != ""
}

Try / catch

ref, err := encodeFn((*graph.Fn)(edge.DoFn))
if err != nil {
  return fmt.Errorf("DoFn %T not serializable (closures/anonymous types unsupported): %w", edge.DoFn, err)
}

Prevention

When it happens

Trigger: Calling EncodeMultiEdge on a graph.MultiEdge whose DoFn cannot be encoded: anonymous/inner functions or closures, unexported types, values not registered for remote marshaling, or structs with unserializable fields.

Common situations: Defining a DoFn as an anonymous struct/inline closure in a Go Beam pipeline submitted to a remote runner; packages renamed/refactored so the recorded funcRef no longer resolves; missing init registration for custom types serialized into fn config.

Understand the failure class

Background: json.Marshal / "failed to marshal" errors in Go: why "unsupported type" happens and how to fix it — this error's family across 22 libraries.

Related errors


AI-assisted analysis of apache/beam@12126d8942 (2026-09-13). Data as JSON: /api/errors/5569d52dcc0c5dc8. Report an issue: GitHub.

Appendix: source

Thrown at sdks/go/pkg/beam/core/runtime/graphx/serialize.go:49

	"github.com/apache/beam/sdks/v2/go/pkg/beam/core/timers"
	"github.com/apache/beam/sdks/v2/go/pkg/beam/core/typex"
	"github.com/apache/beam/sdks/v2/go/pkg/beam/core/util/jsonx"
	"github.com/apache/beam/sdks/v2/go/pkg/beam/core/util/reflectx"
	"github.com/apache/beam/sdks/v2/go/pkg/beam/internal/errors"
)

var genFnType = reflect.TypeOf((*func(string, reflect.Type, []byte) reflectx.Func)(nil)).Elem()

// EncodeMultiEdge converts the preprocessed representation into the wire
// representation of the multiedge, capturing input and output type information.
func EncodeMultiEdge(edge *graph.MultiEdge) (*v1pb.MultiEdge, error) {
	ret := &v1pb.MultiEdge{}
	ret.Opcode = string(edge.Op)

	if edge.DoFn != nil {
		ref, err := encodeFn((*graph.Fn)(edge.DoFn))
		if err != nil {
			wrapped := errors.Wrap(err, "bad userfn")
			return nil, errors.WithContextf(wrapped, "encoding userfn %v", edge)
		}
		ret.Fn = ref
	}
	if edge.CombineFn != nil {
		ref, err := encodeFn((*graph.Fn)(edge.CombineFn))
		if err != nil {
			wrapped := errors.Wrap(err, "bad combinefn")
			return nil, errors.WithContextf(wrapped, "encoding userfn %v", edge)
		}
		ret.Fn = ref
	}
	if edge.WindowFn != nil {
		ret.WindowFn = encodeWindowFn(edge.WindowFn)
	}

	for _, in := range edge.Input {
		kind, err := encodeInputKind(in.Kind)

View on GitHub (pinned to 12126d8942)