plandex-ai/plandex · error

panic in ApplyPlan: %v\n%s

Error message

panic in ApplyPlan: %v\n%s

What it means

ApplyPlan processes pending plan results in parallel goroutines, each wrapped with recover(); a panic inside the goroutine is logged with a stack trace, converted to this error on errCh, and the goroutine exits via runtime.Goexit to prevent a double send.

Source

Thrown at app/server/db/result_helpers.go:576

	numRoutines := len(pendingDbResults) +
		len(convoMessageDescriptions)

	if len(pendingNewFilesSet) > 0 {
		numRoutines++
	}
	if len(pendingUpdatedFilesSet) > 0 {
		numRoutines++
	}

	errCh := make(chan error, numRoutines)
	now := time.Now()

	for _, result := range pendingDbResults {
		go func(result *PlanFileResult) {
			defer func() {
				if r := recover(); r != nil {
					log.Printf("panic in ApplyPlan: %v\n%s", r, debug.Stack())
					errCh <- fmt.Errorf("panic in ApplyPlan: %v\n%s", r, debug.Stack())
					runtime.Goexit() // don't allow outer function to continue and double-send to channel
				}
			}()
			result.AppliedAt = &now

			bytes, err := json.MarshalIndent(result, "", "  ")

			if err != nil {
				errCh <- fmt.Errorf("error marshalling result: %v", err)
				return
			}

			err = os.WriteFile(filepath.Join(resultsDir, result.Id+".json"), bytes, 0644)

			if err != nil {
				errCh <- fmt.Errorf("error writing result file: %v", err)
				return
			}

View on GitHub (pinned to e2d772072e)

Solutions

  1. Read the appended debug.Stack() in the logs to pinpoint the panicking statement
  2. Add nil checks/defensive guards around result field mutation and file writes in the goroutine
  3. Reproduce with the offending result payload in a unit test
  4. Keep the recover+Goexit pattern so one bad result doesn't crash the process

Example fix

// before
go func(result *PlanFileResult) {
    result.AppliedAt = &now
    ...
// after: guard against nil result
if result == nil {
    errCh <- fmt.Errorf("ApplyPlan: nil pending result")
    return
}
result.AppliedAt = &now
Defensive patterns

Strategy: try-catch

Validate before calling

for _, r := range pendingDbResults {
    if r == nil {
        log.Printf("skipping nil pending result before ApplyPlan")
    }
}

Type guard

func validPendingResult(r *PlanFileResult) bool { return r != nil && r.Path != "" }

Try / catch

err := db.ApplyPlan(...)
if err != nil && strings.Contains(err.Error(), "panic in ApplyPlan") {
    // err contains the goroutine stack; attach it to the bug report
    return err
}

Prevention

When it happens

Trigger: Any panic in the goroutine body after the deferred recover: typically nil map/pointer access while mutating result.AppliedAt or writing the marshaled file, or downstream helper bugs under specific result shapes.

Common situations: Concurrent modification of shared result objects; unexpected nil fields in pending results from legacy files; bugs triggered by a specific plan's result payload.

Related errors


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