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
- Read the wrapped inner error for the exact encodeFn failure (type not registered, unresolvable path)
- Replace closures/anonymous DoFns with named top-level types
- Ensure any types used as fn config fields are registered via gob/beam type registration
- 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
- Use named exported DoFn types; never closures or anonymous structs
- Register custom types used in DoFn config fields
- Keep function definitions stable between graph build and submission
- Test pipeline serialization locally with mustEncodeMultiEdgeBase64 before remote submission
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)