{"record":{"id":"ede3aadad38c5632","repo":"plandex-ai/plandex","slug":"panic-in-listplansrunninghandler-v-s","errorCode":null,"errorMessage":"panic in ListPlansRunningHandler: %v\n%s","messagePattern":"panic in ListPlansRunningHandler: (.+?)\n(.+?)","errorType":"panic","errorClass":null,"httpStatus":500,"severity":"critical","filePath":"app/server/handlers/plans_crud.go","lineNumber":448,"sourceCode":"\t\tlog.Printf(\"Error listing plans: %v\\n\", err)\n\t\thttp.Error(w, \"Error listing plans: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tvar planIds []string\n\tfor _, plan := range plans {\n\t\tplanIds = append(planIds, plan.Id)\n\t}\n\n\terrCh := make(chan error, 2)\n\tvar streams []*db.ModelStream\n\tvar branches []*db.Branch\n\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tlog.Printf(\"panic in ListPlansRunningHandler: %v\\n%s\", r, debug.Stack())\n\t\t\t\terrCh <- fmt.Errorf(\"panic in ListPlansRunningHandler: %v\\n%s\", r, debug.Stack())\n\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t}\n\t\t}()\n\n\t\tvar err error\n\t\tif includeRecent {\n\t\t\tstreams, err = db.GetActiveOrRecentModelStreams(planIds)\n\t\t} else {\n\t\t\tstreams, err = db.GetActiveModelStreams(planIds)\n\t\t}\n\t\tif err != nil {\n\t\t\terrCh <- fmt.Errorf(\"error getting recent model streams: %v\", err)\n\t\t\treturn\n\t\t}\n\t\terrCh <- nil\n\t}()\n\n\tgo func() {","sourceCodeStart":430,"sourceCodeEnd":466,"githubUrl":"https://github.com/plandex-ai/plandex/blob/e2d772072efadbe41d2946d97d79be55532dbab5/app/server/handlers/plans_crud.go#L430-L466","documentation":"This is the panic-recovery error emitted by the goroutine in ListPlansRunningHandler (plans_crud.go). When any panic occurs while listing plans/branches (e.g. nil pointer dereference on branch or plan data), the deferred recover converts it into an error 'panic in ListPlansRunningHandler: %v\\n%s' with a stack trace, logs it, sends it on errCh, and calls runtime.Goexit to prevent a double channel send. The caller surfaces it as a 500 with the panic and stack attached.","triggerScenarios":"Any panic inside the listing goroutine: nil pointer dereference when a branch or plan field is unexpectedly nil, index out of range while aggregating running plans, or a type assertion failure on plan/branch data (especially when includeRecent is true and recent-plan data is partially populated).","commonSituations":"Races between plan deletion/archival and listing; corrupted or partially written plan/branch records; upstream refactor changing return types so a nil slips through.","solutions":["Read the attached stack trace (%s part) to locate the panicking line","Add nil checks for plan/branch pointers before dereferencing in the listing loop","Fix any data race by protecting shared plan/branch maps or re-fetching under lock","Deploy the fix and retry the list request"],"exampleFix":"// before\nfor _, b := range plan.Branches {\n    if b.IsRunning {\n// after\nfor _, b := range plan.Branches {\n    if b == nil {\n        continue\n    }\n    if b.IsRunning {\n}","handlingStrategy":"type-guard","validationCode":"if plan == nil || plan.Branches == nil {\n    errCh <- errors.New(\"plan or branches nil while listing running plans\")\n    return\n}","typeGuard":"func safeBranch(b *db.Branch) *db.Branch {\n    if b == nil {\n        return &db.Branch{}\n    }\n    return b\n}\n// usage inside goroutine:\n// b := safeBranch(branch); if b == nil || b.PlanId == \"\" { continue }","tryCatchPattern":"go func() {\n    defer func() {\n        if r := recover(); r != nil {\n            log.Printf(\"panic in ListPlansRunningHandler: %v\\n%s\", r, debug.Stack())\n            errCh <- fmt.Errorf(\"panic in ListPlansRunningHandler: %v\\n%s\", r, debug.Stack())\n            runtime.Goexit()\n        }\n    }()\n    // ... listing logic with nil checks ...\n}()","preventionTips":["Nil-check every plan/branch pointer before dereferencing in loops","Avoid map/slice races by reading shared plan state under a lock","Cover the listing path with tests using partially populated plans","Keep the recover+Goexit pattern so panics never crash the server"],"tags":["go","panic","concurrency"],"backgroundTag":"nil-pointer-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"}