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
- Read the appended debug.Stack() in the logs to pinpoint the panicking statement
- Add nil checks/defensive guards around result field mutation and file writes in the goroutine
- Reproduce with the offending result payload in a unit test
- 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
- Sanitize pending results before applying (no nil pointers/maps)
- Unit-test ApplyPlan with legacy and edge-case result payloads
- Preserve the recover+Goexit pattern; never let panics escape goroutines
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
- panic in GetPlanFileResults: %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/f397fbf744396229.
Report an issue: GitHub.