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

  1. Use the stack trace in the message to pinpoint the panicking line and add nil checks
  2. Ensure GetPlanFileResults never returns nil entries in its slice
  3. Avoid shared mutable *PlanFileResult across goroutines (each goroutine already receives its own result — verify no shared maps/pointers inside)
  4. 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

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


AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05). Data as JSON: /api/errors/b24a32a700ea90a9. Report an issue: GitHub.