apache/beam · error
bad function
Error message
bad function
What it means
DecodeMultiEdge wraps decodeFn failures as 'bad function' (context 'decoding userfn'). When deserializing a graph, the encoded Fn reference (payload + config) must resolve back into a DoFn/CombineFn; failures include unresolvable function paths, missing types, or payload decode errors on the receiving (often remote worker) side.
Solutions
- Read the wrapped decodeFn error: path-resolution vs payload-decode tells you which side is stale
- Rebuild/redeploy the worker with the latest user code so all fn types resolve
- Rebuild the pipeline and resubmit after any package refactor (avoid renaming/moving DoFns between submit and run)
- Register all custom types used in fn payloads so payload unmarshaling succeeds
Example fix
// before // worker binary built before adding type myFn; graph references myFn -> bad function // after go build -o worker ./cmd/worker // rebuild worker with current user code, then resubmit
Defensive patterns
Strategy: try-catch
Validate before calling
func canDecodeFn(ref *pipepb.FunctionRef) error {
fnT, err := typeForPath(ref.GetFn().GetPath())
if err != nil { return fmt.Errorf("fn path %q unresolved: %w", ref.GetFn().GetPath(), err) }
return validatePayloadTypes(ref, fnT)
} Type guard
func fnResolvable(path string) bool {
_, err := typeForPath(path)
return err == nil
} Try / catch
u, err := decodeFn(edge.Fn)
if err != nil {
return fmt.Errorf("decoding userfn %v: fn %q missing or payload invalid: %w", edge, edge.Fn.GetFn().GetPath(), err)
} Prevention
- Rebuild worker binaries whenever user code changes
- Avoid renaming/moving DoFn packages between graph construction and execution
- Keep submit-side and worker-side Beam versions identical
- Register all custom payload types in worker init()
When it happens
Trigger: DecodeMultiEdge called from makeLink on an edge with edge.Fn != nil where decodeFn fails: the fn's package path/type cannot be resolved (code not on the worker), the serialized payload cannot be unmarshaled, or registered types are missing in the decoding process.
Common situations: Remote execution where the worker binary is stale and lacks the pipeline's DoFn types; package refactor moving functions so recorded paths no longer resolve; version mismatch between submitting SDK and worker; unregistered custom config types.
Understand the failure class
Background: "cannot parse invalid wire-format data", "cannot unmarshal", "failed unmarshalling": protobuf unmarshal errors explained — this error's family across 10 libraries.
Related errors
AI-assisted analysis of apache/beam@12126d8942 (2026-09-13).
Data as JSON: /api/errors/63e38b8f0729cc40.
Report an issue: GitHub.
Appendix: source
Thrown at sdks/go/pkg/beam/core/runtime/graphx/serialize.go:105
return ret, nil
}
// DecodeMultiEdge converts the wire representation into the preprocessed
// components representing that edge. We deserialize to components to avoid
// inserting the edge into a graph or creating a detached edge.
func DecodeMultiEdge(edge *v1pb.MultiEdge) (graph.Opcode, *graph.Fn, *window.Fn, []*graph.Inbound, []*graph.Outbound, error) {
var u *graph.Fn
var wfn *window.Fn
var inbound []*graph.Inbound
var outbound []*graph.Outbound
opcode := graph.Opcode(edge.Opcode)
if edge.Fn != nil {
var err error
u, err = decodeFn(edge.Fn)
if err != nil {
wrapped := errors.Wrap(err, "bad function")
return "", nil, nil, nil, nil, errors.WithContextf(wrapped, "decoding userfn %v", edge)
}
}
if edge.WindowFn != nil {
wfn = decodeWindowFn(edge.WindowFn)
}
for _, in := range edge.Inbound {
kind, err := decodeInputKind(in.Kind)
if err != nil {
wrapped := errors.Wrap(err, "bad input kind")
return "", nil, nil, nil, nil, errors.WithContextf(wrapped, "decoding userfn %v", edge)
}
t, err := decodeFullType(in.Type)
if err != nil {
wrapped := errors.Wrap(err, "bad input type")
return "", nil, nil, nil, nil, errors.WithContextf(wrapped, "decoding userfn %v", edge)
}
inbound = append(inbound, &graph.Inbound{Kind: kind, Type: t})View on GitHub (pinned to 12126d8942)