{"record":{"id":"962c411f95439837","repo":"plandex-ai/plandex","slug":"panic-in-syncplantokens-v-s","errorCode":null,"errorMessage":"panic in SyncPlanTokens: %v\n%s","messagePattern":"panic in SyncPlanTokens: (.+?)\n(.+?)","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/server/db/plan_helpers.go","lineNumber":204,"sourceCode":"\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error updating plan tokens: %v\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc SyncPlanTokens(orgId, planId, branch string) error {\n\tvar contexts []*Context\n\tvar convos []*ConvoMessage\n\terrCh := make(chan error, 2)\n\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tlog.Printf(\"panic in SyncPlanTokens: %v\\n%s\", r, debug.Stack())\n\t\t\t\terrCh <- fmt.Errorf(\"panic in SyncPlanTokens: %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\t\tvar err error\n\t\tcontexts, err = GetPlanContexts(orgId, planId, false, false)\n\t\terrCh <- err\n\t}()\n\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tlog.Printf(\"panic in SyncPlanTokens: %v\\n%s\", r, debug.Stack())\n\t\t\t\terrCh <- fmt.Errorf(\"panic in SyncPlanTokens: %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\t\tvar err error\n\t\tconvos, err = GetPlanConvo(orgId, planId)","sourceCodeStart":186,"sourceCodeEnd":222,"githubUrl":"https://github.com/plandex-ai/plandex/blob/e2d772072efadbe41d2946d97d79be55532dbab5/app/server/db/plan_helpers.go#L186-L222","documentation":"SyncPlanTokens launches two goroutines, each with a recover() guard that converts a panic into an error sent over errCh. This message means the FIRST goroutine — the one calling GetPlanContexts(orgId, planId, false, false) — panicked, the panic was logged with a stack trace, and runtime.Goexit() stopped that goroutine. The error is subsequently surfaced by the collector loop (see error 454).","triggerScenarios":"GetPlanContexts or code it calls (nil map/slice access, nil pointer deref on a Context row, unexpected sqlx result shape) panics inside the goroutine that assigns `contexts` and sends `err` to errCh.","commonSituations":"Corrupt or partially-null context rows in the DB causing nil dereference during mapping; concurrent map/plan mutation elsewhere; a schema change making sqlx scan into an unexpected type; orgId/planId being empty strings hitting an unguarded code path.","solutions":["Check the server log — the panic was logged with a full stack trace ('panic in SyncPlanTokens') — and fix the panicking function identified there","Validate orgId and planId are non-empty before calling SyncPlanTokens","Update GetPlanContexts to defensively handle nil rows/fields returned from the DB","Confirm schema matches the Context struct after recent migrations"],"exampleFix":"// before\ncontexts, err = GetPlanContexts(orgId, planId, false, false)\nerrCh <- err\n// after\nif orgId == \"\" || planId == \"\" {\n    errCh <- fmt.Errorf(\"SyncPlanTokens: empty orgId or planId\")\n    return\n}\ncontexts, err = GetPlanContexts(orgId, planId, false, false)\nerrCh <- err","handlingStrategy":"try-catch","validationCode":"if orgId == \"\" || planId == \"\" {\n    return fmt.Errorf(\"SyncPlanTokens requires non-empty orgId and planId\")\n}","typeGuard":"func validPlanRef(orgId, planId string) bool {\n    return orgId != \"\" && planId != \"\"\n}","tryCatchPattern":"if err := SyncPlanTokens(orgId, planId, branch); err != nil {\n    if strings.Contains(err.Error(), \"panic in SyncPlanTokens\") {\n        log.Printf(\"panic recovered; see stack in server log\")\n    }\n}","preventionTips":["Grep server logs for 'panic in SyncPlanTokens' and fix the stack trace's root cause","Add nil checks in GetPlanContexts row mapping","Keep the Context struct in sync with the contexts table schema","Never ignore recovered panics — they indicate real bugs, not transient failures"],"tags":["go","panic","concurrency","database"],"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-14T05:17:10.506Z"}