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

  1. Check the server log — the panic was logged with a full stack trace ('panic in SyncPlanTokens') — and fix the panicking function identified there
  2. Validate orgId and planId are non-empty before calling SyncPlanTokens
  3. Update GetPlanContexts to defensively handle nil rows/fields returned from the DB
  4. 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

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


AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05). Data as JSON: /api/errors/962c411f95439837. Report an issue: GitHub.