{"record":{"id":"da3f851559667d0d","repo":"wavetermdev/waveterm","slug":"component-function-must-be-a-function-da3f85","errorCode":null,"errorMessage":"Component function must be a function","messagePattern":"Component function must be a function","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"tsunami/engine/rootelem.go","lineNumber":242,"sourceCode":"\t\treturn fmt.Errorf(\"atom %q not found\", name)\n\t}\n\treturn atom.SetVal(val)\n}\n\nfunc (r *RootElem) RemoveAtom(name string) {\n\tr.atomLock.Lock()\n\tdefer r.atomLock.Unlock()\n\n\tdelete(r.Atoms, name)\n}\n\nfunc validateCFunc(cfunc any) error {\n\tif cfunc == nil {\n\t\treturn fmt.Errorf(\"Component function cannot b nil\")\n\t}\n\trval := reflect.ValueOf(cfunc)\n\tif rval.Kind() != reflect.Func {\n\t\treturn fmt.Errorf(\"Component function must be a function\")\n\t}\n\trtype := rval.Type()\n\tif rtype.NumIn() != 1 {\n\t\treturn fmt.Errorf(\"Component function must take exactly 1 argument\")\n\t}\n\tif rtype.NumOut() != 1 {\n\t\treturn fmt.Errorf(\"Component function must return exactly 1 value\")\n\t}\n\t// first argument can be a map[string]any, or a struct, or ptr to struct (we'll reflect the value into it)\n\targ1Type := rtype.In(0)\n\tif arg1Type.Kind() == reflect.Ptr {\n\t\targ1Type = arg1Type.Elem()\n\t}\n\tif arg1Type.Kind() == reflect.Map {\n\t\tif arg1Type.Key().Kind() != reflect.String ||\n\t\t\t!(arg1Type.Elem().Kind() == reflect.Interface && arg1Type.Elem().NumMethod() == 0) {\n\t\t\treturn fmt.Errorf(\"Map argument must be map[string]any\")\n\t\t}","sourceCodeStart":224,"sourceCodeEnd":260,"githubUrl":"https://github.com/wavetermdev/waveterm/blob/a4447c1563b2df285ab89e76c82f91e1a1a49c1e/tsunami/engine/rootelem.go#L224-L260","documentation":"RegisterComponent only accepts component functions, but validateCFunc uses reflect to check that cfunc's reflect.Kind is Func. Passing any non-function value (string, int, struct, map) triggers \"Component function must be a function\". This guards the deferred reflect.Call path from panicking later.","triggerScenarios":"RegisterComponent(\"name\", someStringOrStruct); passing a variable of type any that actually holds a non-func value; passing a method value wrapper that was reassigned to data.","commonSituations":"Confusing RegisterComponent with an API that takes component state/data objects; accidentally passing the result of a lookup that returned data instead of a handler; refactoring that changed the registered value from a func to a struct.","solutions":["Pass an actual function value to RegisterComponent","Verify the variable's concrete type before registering (reflect.TypeOf or an IDE type check)","If the handler comes from a map/registry, assert it is a func before registration"],"exampleFix":"// before\nroot.RegisterComponent(\"panel\", PanelConfig{}) // struct, not a func\n// after\nroot.RegisterComponent(\"panel\", func(props map[string]any) any {\n    return renderPanel(props)\n})","handlingStrategy":"type-guard","validationCode":"if reflect.ValueOf(cfunc).Kind() != reflect.Func {\n    return fmt.Errorf(\"component %q: value is %T, need a func\", name, cfunc)\n}","typeGuard":"func isFunc(v any) bool { return v != nil && reflect.ValueOf(v).Kind() == reflect.Func }","tryCatchPattern":"if err := root.RegisterComponent(\"panel\", v); err != nil {\n    return fmt.Errorf(\"bad component value for panel: %w\", err)\n}","preventionTips":["Register only literal func values, not data/config objects","Keep a single registration helper that validates types once","Let the compiler check types by avoiding any-typed handler variables"],"tags":["go","reflection","type-mismatch","registration"],"backgroundTag":"callback-signature-mismatch","analyzedSha":"a4447c1563b2df285ab89e76c82f91e1a1a49c1e","analyzedAt":"2026-09-01T15:26:23.972Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}