plandex-ai/plandex · error
panic in SyncPlanTokens: %v %s
Error message
panic in SyncPlanTokens: %v %s
What it means
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).
Source
Thrown at app/server/db/plan_helpers.go:204
err := <-errCh
if err != nil {
return fmt.Errorf("error updating plan tokens: %v", err)
}
}
return nil
}
func SyncPlanTokens(orgId, planId, branch string) error {
var contexts []*Context
var convos []*ConvoMessage
errCh := make(chan error, 2)
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in SyncPlanTokens: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in SyncPlanTokens: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
var err error
contexts, err = GetPlanContexts(orgId, planId, false, false)
errCh <- err
}()
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in SyncPlanTokens: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in SyncPlanTokens: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
var err error
convos, err = GetPlanConvo(orgId, planId)View on GitHub (pinned to e2d772072e)
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
Example fix
// before
contexts, err = GetPlanContexts(orgId, planId, false, false)
errCh <- err
// after
if orgId == "" || planId == "" {
errCh <- fmt.Errorf("SyncPlanTokens: empty orgId or planId")
return
}
contexts, err = GetPlanContexts(orgId, planId, false, false)
errCh <- err Defensive patterns
Strategy: try-catch
Validate before calling
if orgId == "" || planId == "" {
return fmt.Errorf("SyncPlanTokens requires non-empty orgId and planId")
} Type guard
func validPlanRef(orgId, planId string) bool {
return orgId != "" && planId != ""
} Try / catch
if err := SyncPlanTokens(orgId, planId, branch); err != nil {
if strings.Contains(err.Error(), "panic in SyncPlanTokens") {
log.Printf("panic recovered; see stack in server log")
}
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- error getting current plan state: %v
- error getting plan convo: %v
- panic in UpdateContexts: %v\n%s
- panic in GetPlanConvo: %v\n%s
- panic in gitRemoveIndexLockFileIfExists: %v %s
AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05).
Data as JSON: /api/errors/962c411f95439837.
Report an issue: GitHub.