{"record":{"id":"7c687029c21fbc1d","repo":"plandex-ai/plandex","slug":"panic-in-getconvomessagedescriptions-v-n-s","errorCode":null,"errorMessage":"panic in GetConvoMessageDescriptions: %v\\n%s","messagePattern":"panic in GetConvoMessageDescriptions: (.+?)\\\\n(.+?)","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"app/server/db/result_helpers.go","lineNumber":331,"sourceCode":"\n\tif err != nil {\n\n\t\tif os.IsNotExist(err) {\n\t\t\treturn descriptions, nil\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"error reading descriptions dir: %v\", err)\n\t}\n\n\terrCh := make(chan error, len(files))\n\tdescCh := make(chan *ConvoMessageDescription, len(files))\n\n\tfor _, file := range files {\n\t\tgo func(file os.DirEntry) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in GetConvoMessageDescriptions: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic in GetConvoMessageDescriptions: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\t\t\tpath := filepath.Join(descriptionsDir, file.Name())\n\n\t\t\tbytes, err := os.ReadFile(path)\n\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error reading description file %s: %v\", file.Name(), err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tvar description ConvoMessageDescription\n\t\t\terr = json.Unmarshal(bytes, &description)\n\n\t\t\tif err != nil {\n\t\t\t\tlog.Println(\"Error unmarshalling description file:\", path)\n\t\t\t\tlog.Println(\"bytes:\")","sourceCodeStart":313,"sourceCodeEnd":349,"githubUrl":"https://github.com/plandex-ai/plandex/blob/e2d772072efadbe41d2946d97d79be55532dbab5/app/server/db/result_helpers.go#L313-L349","documentation":"GetConvoMessageDescriptions spawns one goroutine per description file, each guarded by a deferred recover(). If reading or parsing a file panics (nil deref, out-of-range, etc.), the recover converts the panic value plus debug.Stack() into 'panic in GetConvoMessageDescriptions: %v\\n%s', sends it on errCh, and calls runtime.Goexit to stop the goroutine without double-sending.","triggerScenarios":"A panic occurs inside a per-file goroutine (result_helpers.go:327-357) — e.g. during os.ReadFile, json.Unmarshal into ConvoMessageDescription, or while handling a file whose Name() or path is unexpected (special files, entries vanished mid-iteration).","commonSituations":"Race between ReadDir and file deletion (file removed between listing and ReadFile in exotic setups); extremely large or zero-length files causing unexpected library panics; custom UnmarshalJSON panicking on malformed data; memory pressure under very large plans with thousands of concurrent per-file goroutines.","solutions":["Extract the stack trace from the error message and locate the panicking line in the per-file goroutine","Check the server log for the identical log.Printf trace for goroutine context","Identify the specific description file involved and inspect/repair or remove it","Ensure ConvoMessageDescription (and any custom UnmarshalJSON) handles nil/empty input without panicking","Consider bounding per-file goroutine concurrency with a worker pool if memory pressure triggers panics"],"exampleFix":"// before\nbytes, err := os.ReadFile(path)\nif err != nil {\n    errCh <- fmt.Errorf(\"error reading description file %s: %v\", file.Name(), err)\n    return\n}\n// after\nif !file.Type().IsRegular() {\n    descCh <- nil // skip non-regular entries like sockets/dirs\n    return\n}\nbytes, err := os.ReadFile(path)\nif err != nil {\n    errCh <- fmt.Errorf(\"error reading description file %s: %v\", file.Name(), err)\n    return\n}","handlingStrategy":"try-catch","validationCode":"// list and pre-screen entries before triggering per-file goroutines\nentries, err := os.ReadDir(getPlanDescriptionsDir(orgId, planId))\nif err == nil {\n    for _, e := range entries {\n        if !e.Type().IsRegular() {\n            log.Printf(\"skipping non-regular description entry: %s\", e.Name())\n        }\n    }\n}","typeGuard":"func isDescParsePanicError(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"panic in GetConvoMessageDescriptions\")\n}","tryCatchPattern":"descriptions, err := GetConvoMessageDescriptions(orgId, planId)\nif err != nil {\n    if isDescParsePanicError(err) {\n        log.Printf(\"per-file goroutine panic, stack embedded:\\n%s\", err)\n        return nil, err\n    }\n    return nil, err\n}","preventionTips":["Keep only regular files inside descriptions directories (no subdirs/sockets)","Ensure any custom UnmarshalJSON on ConvoMessageDescription cannot panic on malformed input","Bound concurrency for very large plans to avoid memory-pressure panics","Log and archive the embedded stack trace on every occurrence"],"tags":["go","panic","concurrency","recovered-panic","json"],"backgroundTag":"goroutine-panic-recovered","analyzedSha":"e2d772072efadbe41d2946d97d79be55532dbab5","analyzedAt":"2026-09-05T20:56:53.631Z","contentChangedAt":"2026-09-05T20:56:53.631Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}