{"record":{"id":"c40faefd58f099ba","repo":"plandex-ai/plandex","slug":"panic-in-deletedraftplans-v-s","errorCode":null,"errorMessage":"panic in DeleteDraftPlans: %v\n%s","messagePattern":"panic in DeleteDraftPlans: (.+?)\n(.+?)","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/server/db/plan_helpers.go","lineNumber":378,"sourceCode":"\t// get ids\n\tvar ids []string\n\n\tfor res.Next() {\n\t\tvar id string\n\t\terr := res.Scan(&id)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error scanning deleted draft plan id: %v\", err)\n\t\t}\n\t\tids = append(ids, id)\n\t}\n\n\terrCh := make(chan error, len(ids))\n\tfor _, planId := range ids {\n\t\tgo func(planId string) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in DeleteDraftPlans: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic in DeleteDraftPlans: %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\terrCh <- DeletePlanDir(orgId, planId)\n\t\t}(planId)\n\t}\n\n\tfor i := 0; i < len(ids); i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error deleting draft plan dir: %v\", err)\n\t\t}\n\t}\n\n\tif len(ids) > 0 {\n\t\tlog.Println(\"Deleted\", len(ids), \"draft plans\")\n\t}\n","sourceCodeStart":360,"sourceCodeEnd":396,"githubUrl":"https://github.com/plandex-ai/plandex/blob/e2d772072efadbe41d2946d97d79be55532dbab5/app/server/db/plan_helpers.go#L360-L396","documentation":"DeleteDraftPlans spawns goroutines to delete each draft plan's directory. This message is both logged and sent to errCh when one of those goroutines panics; the deferred recover captures it, and runtime.Goexit prevents double-sending to the channel. It indicates an unexpected runtime fault (nil map write, index out of range, nil pointer) inside DeletePlanDir.","triggerScenarios":"A goroutine executing DeletePlanDir(orgId, planId) panics: nil dereference inside path handling, os operations with malformed planId, or an unrecovered panic deeper in the call stack.","commonSituations":"planId containing path-traversal/empty values causing odd filesystem behavior; a bug introduced in DeletePlanDir; concurrent map access elsewhere; stack exhaustion from recursive delete loops (symlink cycles).","solutions":["Read the logged stack trace to find the panicking frame in DeletePlanDir","Guard against empty/invalid planId before spawning goroutines","Make DeletePlanDir return errors instead of panicking on bad input","Handle symlink cycles with proper path validation before os.RemoveAll"],"exampleFix":"// before\nerrCh <- DeletePlanDir(orgId, planId)\n// after\nif planId == \"\" {\n    errCh <- fmt.Errorf(\"empty planId\")\n    return\n}\nerrCh <- DeletePlanDir(orgId, planId)","handlingStrategy":"try-catch","validationCode":"if planId == \"\" {\n    return fmt.Errorf(\"cannot delete plan dir: empty planId\")\n}","typeGuard":null,"tryCatchPattern":"if err := DeleteDraftPlans(orgId, projectId, userId); err != nil {\n    if strings.Contains(err.Error(), \"panic in DeleteDraftPlans\") {\n        // inspect the attached debug.Stack() to find the panicking frame\n    }\n    return err\n}","preventionTips":["Make DeletePlanDir error-returning, never panicking","Validate planId (non-empty, no path traversal) before goroutine spawn","Avoid symlink cycles in plan directories","Keep recover+Goexit pattern intact to prevent double channel sends"],"tags":["panic","concurrency","goroutine","go"],"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"}