plandex-ai/plandex · error
panic in RejectPlanFile: %v\n%s
Error message
panic in RejectPlanFile: %v\n%s
What it means
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.
Source
Thrown at app/server/db/result_helpers.go:954
return nil
}
func RejectPlanFile(orgId, planId, filePathOrResultId string, now time.Time) error {
resultsDir := getPlanResultsDir(orgId, planId)
results, err := GetPlanFileResults(orgId, planId)
if err != nil {
return fmt.Errorf("error getting plan file results: %v", err)
}
errCh := make(chan error, len(results))
for _, result := range results {
go func(result *PlanFileResult) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in RejectPlanFile: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in RejectPlanFile: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
if (result.Path == filePathOrResultId || result.Id == filePathOrResultId) && result.AppliedAt == nil && result.RejectedAt == nil {
result.RejectedAt = &now
} else {
errCh <- nil
return
}
bytes, err := json.MarshalIndent(result, "", " ")
if err != nil {
errCh <- fmt.Errorf("error marshalling result: %v", err)
}
err = os.WriteFile(filepath.Join(resultsDir, result.Id+".json"), bytes, 0644)
View on GitHub (pinned to e2d772072e)
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
Example fix
// before
if (result.Path == filePathOrResultId || result.Id == filePathOrResultId) && result.AppliedAt == nil && result.RejectedAt == nil {
// after
if result != nil && (result.Path == filePathOrResultId || result.Id == filePathOrResultId) && result.AppliedAt == nil && result.RejectedAt == nil { Defensive patterns
Strategy: type-guard
Validate before calling
results, err := GetPlanFileResults(orgId, planId)
if err != nil { return err }
for _, r := range results {
if r == nil { return fmt.Errorf("nil result entry from GetPlanFileResults") }
} Type guard
func isValidPlanFileResult(r *PlanFileResult) bool {
return r != nil && r.Id != "" && r.Path != ""
} Try / catch
err := RejectPlanFile(orgId, planId, filePathOrResultId, now)
if err != nil && strings.HasPrefix(err.Error(), "panic in RejectPlanFile") {
log.Printf("panic recovered: %v", err) // stack trace included for diagnosis
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: GetPlanFileResults returned a slice containing nil entries; data race corrupting a shared *PlanFileResult across goroutines; unexpected nil pointer after schema change.
Related errors
- panic in RejectPlanFiles: %v\n%s
- panic in UpdateContexts: %v\n%s
- panic in GetPlanConvo: %v\n%s
- panic in gitRemoveIndexLockFileIfExists: %v %s
- panic in SyncPlanTokens: %v %s
AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05).
Data as JSON: /api/errors/b24a32a700ea90a9.
Report an issue: GitHub.