{"record":{"id":"f397fbf744396229","repo":"plandex-ai/plandex","slug":"panic-in-applyplan-v-n-s","errorCode":null,"errorMessage":"panic in ApplyPlan: %v\\n%s","messagePattern":"panic in ApplyPlan: (.+?)\\\\n(.+?)","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/server/db/result_helpers.go","lineNumber":576,"sourceCode":"\tnumRoutines := len(pendingDbResults) +\n\t\tlen(convoMessageDescriptions)\n\n\tif len(pendingNewFilesSet) > 0 {\n\t\tnumRoutines++\n\t}\n\tif len(pendingUpdatedFilesSet) > 0 {\n\t\tnumRoutines++\n\t}\n\n\terrCh := make(chan error, numRoutines)\n\tnow := time.Now()\n\n\tfor _, result := range pendingDbResults {\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 ApplyPlan: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic in ApplyPlan: %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\tresult.AppliedAt = &now\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\treturn\n\t\t\t}\n\n\t\t\terr = os.WriteFile(filepath.Join(resultsDir, result.Id+\".json\"), bytes, 0644)\n\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error writing result file: %v\", err)\n\t\t\t\treturn\n\t\t\t}","sourceCodeStart":558,"sourceCodeEnd":594,"githubUrl":"https://github.com/plandex-ai/plandex/blob/e2d772072efadbe41d2946d97d79be55532dbab5/app/server/db/result_helpers.go#L558-L594","documentation":"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.","triggerScenarios":"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.","commonSituations":"Concurrent modification of shared result objects; unexpected nil fields in pending results from legacy files; bugs triggered by a specific plan's result payload.","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"],"exampleFix":"// before\ngo func(result *PlanFileResult) {\n    result.AppliedAt = &now\n    ...\n// after: guard against nil result\nif result == nil {\n    errCh <- fmt.Errorf(\"ApplyPlan: nil pending result\")\n    return\n}\nresult.AppliedAt = &now","handlingStrategy":"try-catch","validationCode":"for _, r := range pendingDbResults {\n    if r == nil {\n        log.Printf(\"skipping nil pending result before ApplyPlan\")\n    }\n}","typeGuard":"func validPendingResult(r *PlanFileResult) bool { return r != nil && r.Path != \"\" }","tryCatchPattern":"err := db.ApplyPlan(...)\nif err != nil && strings.Contains(err.Error(), \"panic in ApplyPlan\") {\n    // err contains the goroutine stack; attach it to the bug report\n    return err\n}","preventionTips":["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"],"tags":["go","panic","concurrency","stack-trace"],"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"}