{"record":{"id":"b24a32a700ea90a9","repo":"plandex-ai/plandex","slug":"panic-in-rejectplanfile-v-n-s","errorCode":null,"errorMessage":"panic in RejectPlanFile: %v\\n%s","messagePattern":"panic in RejectPlanFile: (.+?)\\\\n(.+?)","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/server/db/result_helpers.go","lineNumber":954,"sourceCode":"\treturn nil\n}\n\nfunc RejectPlanFile(orgId, planId, filePathOrResultId string, now time.Time) error {\n\tresultsDir := getPlanResultsDir(orgId, planId)\n\tresults, err := GetPlanFileResults(orgId, planId)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error getting plan file results: %v\", err)\n\t}\n\n\terrCh := make(chan error, len(results))\n\n\tfor _, result := range results {\n\t\tgo func(result *PlanFileResult) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in RejectPlanFile: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic in RejectPlanFile: %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\tif (result.Path == filePathOrResultId || result.Id == filePathOrResultId) && result.AppliedAt == nil && result.RejectedAt == nil {\n\t\t\t\tresult.RejectedAt = &now\n\t\t\t} else {\n\t\t\t\terrCh <- nil\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tbytes, err := json.MarshalIndent(result, \"\", \"  \")\n\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error marshalling result: %v\", err)\n\t\t\t}\n\n\t\t\terr = os.WriteFile(filepath.Join(resultsDir, result.Id+\".json\"), bytes, 0644)\n","sourceCodeStart":936,"sourceCodeEnd":972,"githubUrl":"https://github.com/plandex-ai/plandex/blob/e2d772072efadbe41d2946d97d79be55532dbab5/app/server/db/result_helpers.go#L936-L972","documentation":"RejectPlanFile processes each result in its own goroutine guarded by recover(); a panic there is converted to 'panic in RejectPlanFile: %v\\n%s' with a stack trace, sent on errCh, and the goroutine exits via runtime.Goexit to prevent a double channel send.","triggerScenarios":"Panic inside the per-result goroutine body: nil PlanFileResult pointer in the results slice, dereferencing result.Path/Id/AppliedAt on nil, or a panic from MarshalIndent/WriteFile helpers.","commonSituations":"GetPlanFileResults returned a slice containing nil entries; data race corrupting a shared *PlanFileResult across goroutines; unexpected nil pointer after schema change.","solutions":["Use the stack trace in the message to pinpoint the panicking line and add nil checks","Ensure GetPlanFileResults never returns nil entries in its slice","Avoid shared mutable *PlanFileResult across goroutines (each goroutine already receives its own result — verify no shared maps/pointers inside)","Change helpers to return errors rather than panic"],"exampleFix":"// before\nif (result.Path == filePathOrResultId || result.Id == filePathOrResultId) && result.AppliedAt == nil && result.RejectedAt == nil {\n// after\nif result != nil && (result.Path == filePathOrResultId || result.Id == filePathOrResultId) && result.AppliedAt == nil && result.RejectedAt == nil {","handlingStrategy":"type-guard","validationCode":"results, err := GetPlanFileResults(orgId, planId)\nif err != nil { return err }\nfor _, r := range results {\n    if r == nil { return fmt.Errorf(\"nil result entry from GetPlanFileResults\") }\n}","typeGuard":"func isValidPlanFileResult(r *PlanFileResult) bool {\n    return r != nil && r.Id != \"\" && r.Path != \"\"\n}","tryCatchPattern":"err := RejectPlanFile(orgId, planId, filePathOrResultId, now)\nif err != nil && strings.HasPrefix(err.Error(), \"panic in RejectPlanFile\") {\n    log.Printf(\"panic recovered: %v\", err) // stack trace included for diagnosis\n}","preventionTips":["Guarantee GetPlanFileResults never appends nil pointers to its returned slice","Validate each *PlanFileResult (Id/Path non-empty) before fan-out","Keep the recover + runtime.Goexit pattern to avoid double channel sends","Avoid data races: never mutate shared PlanFileResult state across goroutines"],"tags":["go","panic","nil-pointer","concurrency","recover"],"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-12T22:17:10.623Z"}