{"record":{"id":"4b642aa228c8f2d9","repo":"plandex-ai/plandex","slug":"panic-in-deleteownerplans-v-s","errorCode":null,"errorMessage":"panic in DeleteOwnerPlans: %v\n%s","messagePattern":"panic in DeleteOwnerPlans: (.+?)\n(.+?)","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/server/db/plan_helpers.go","lineNumber":426,"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 DeleteOwnerPlans: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic in DeleteOwnerPlans: %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 plan dir: %v\", err)\n\t\t}\n\t}\n\n\tif len(ids) > 0 {\n\t\tlog.Println(\"Deleted\", len(ids), \"plans\")\n\t}\n","sourceCodeStart":408,"sourceCodeEnd":444,"githubUrl":"https://github.com/plandex-ai/plandex/blob/e2d772072efadbe41d2946d97d79be55532dbab5/app/server/db/plan_helpers.go#L408-L444","documentation":"DeleteOwnerPlans deletes each plan's directory in a separate goroutine. If any goroutine panics (e.g. inside DeletePlanDir), the deferred recover() logs the panic with a stack trace, converts it into this error, sends it to errCh, and calls runtime.Goexit() so the worker doesn't double-send. The caller surfaces the panic as a normal error value instead of crashing the process.","triggerScenarios":"A panic is raised inside DeletePlanDir (called as `DeletePlanDir(orgId, planId)` in the spawned goroutine) — e.g. nil pointer dereference, out-of-range slice access, or an unexpected nil from a lower layer — while iterating over plan ids.","commonSituations":"Corrupted or unexpected plan directory state on disk, races on shared state inside DeletePlanDir, or a nil map/pointer bug introduced by a recent change to DeletePlanDir.","solutions":["Read the attached stack trace to find the panicking line inside DeletePlanDir and fix the underlying nil/pointer bug","Check the plan id and orgId values passed to DeleteOwnerPlans for empty or invalid inputs","Re-run after fixing; the error is per-plan, so other plans in the batch may still have been processed","If panics recur, add validation before spawning goroutines instead of relying on recover"],"exampleFix":"// before\nerrCh <- DeletePlanDir(orgId, planId)\n// after\nif planId == \"\" {\n    errCh <- fmt.Errorf(\"cannot delete plan: empty planId\")\n    return\n}\nerrCh <- DeletePlanDir(orgId, planId)","handlingStrategy":"try-catch","validationCode":"if planId == \"\" || orgId == \"\" {\n    return fmt.Errorf(\"invalid args: orgId/planId must be non-empty\")\n}","typeGuard":null,"tryCatchPattern":"err := DeleteOwnerPlans(orgId, ids)\nif err != nil && strings.Contains(err.Error(), \"panic in DeleteOwnerPlans\") {\n    log.Printf(\"worker panic during plan deletion: %v\", err)\n    // retry or alert; panic was already recovered inside the library\n}","preventionTips":["Validate orgId and plan ids before calling DeleteOwnerPlans","Keep DeletePlanDir free of shared mutable state to avoid data races","Add unit tests covering empty/invalid plan directories","Monitor the logs for 'panic in DeleteOwnerPlans' to catch underlying bugs early"],"tags":["go","panic","filesystem","concurrency"],"backgroundTag":"recovered-panic","analyzedSha":"e2d772072efadbe41d2946d97d79be55532dbab5","analyzedAt":"2026-09-05T20:56:53.631Z","contentChangedAt":"2026-09-05T20:56:53.631Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}