plandex-ai/plandex · error
error incrementing num non draft plans: %v
Error message
error incrementing num non draft plans: %v
What it means
This error wraps a failure from db.IncNumNonDraftPlans, the second statement in the same 'rename plan' transaction, which increments the user's count of non-draft plans after a successful rename. If this UPDATE fails the transaction is rolled back (the rename is undone too) and the wrapped error is returned. It indicates the bookkeeping counter could not be updated even though the rename itself may have succeeded inside the tx.
Source
Thrown at app/server/model/plan/tell_load.go:125
if err != nil {
log.Printf("Error generating plan name: %v\n", err)
errCh <- fmt.Errorf("error generating plan name: %v", err)
return
}
err = db.WithTx(active.Ctx, "rename plan", func(tx *sqlx.Tx) error {
err := db.RenamePlan(planId, name, tx)
if err != nil {
log.Printf("Error renaming plan: %v\n", err)
return fmt.Errorf("error renaming plan: %v", err)
}
err = db.IncNumNonDraftPlans(currentUserId, tx)
if err != nil {
log.Printf("Error incrementing num non draft plans: %v\n", err)
return fmt.Errorf("error incrementing num non draft plans: %v", err)
}
return nil
})
if err != nil {
log.Printf("Error renaming plan: %v\n", err)
errCh <- fmt.Errorf("error renaming plan: %v", err)
return
}
}
errCh <- nil
}()
go func() {
defer func() {
if r := recover(); r != nil {View on GitHub (pinned to e2d772072e)
Solutions
- Inspect the wrapped inner error and 'Error incrementing num non draft plans' log to find the root SQL cause
- Retry the request — the transaction rolled back so the draft is intact and the whole rename can be redone
- Verify the user-stats row for currentUserId exists and the IncNumNonDraftPlans SQL matches the current schema
- Check DB logs for deadlocks or lock waits on the user row during the transaction
- Wrap the flow with a bounded retry for transient connection/deadlock errors
Defensive patterns
Strategy: try-catch
Validate before calling
// before the transaction
var exists bool
err := db.Get(&exists, "SELECT EXISTS(SELECT 1 FROM users WHERE id = $1)", currentUserId)
if err != nil || !exists {
return fmt.Errorf("user %s not found; cannot increment non-draft plan count", currentUserId)
} Type guard
func incErrIsRollbackSafe(err error) bool {
// WithTx rolls back on any returned error; classify for retry decisions
var pgErr *pgconn.PgError
return errors.As(err, &pgErr) && (pgErr.Code == "40001" || pgErr.Code == "40P01")
} Try / catch
err = db.WithTx(active.Ctx, "rename plan", func(tx *sqlx.Tx) error {
if err := db.RenamePlan(planId, name, tx); err != nil {
return err
}
if err := db.IncNumNonDraftPlans(currentUserId, tx); err != nil {
return fmt.Errorf("error incrementing num non draft plans: %w", err)
}
return nil
})
if err != nil {
log.Printf("rename tx rolled back: %v", err)
return err
} Prevention
- Ensure the user statistics row exists (upsert on user creation) so the UPDATE always affects a row
- Keep IncNumNonDraftPlans SQL in sync with schema migrations
- Keep transactions short to reduce deadlock risk on the user row
- Use bounded retries for serialization-failure (40001) and deadlock (40P01) errors
- Rely on WithTx rollback so a failed increment never leaves a half-renamed plan
When it happens
Trigger: After a successful db.RenamePlan inside db.WithTx, db.IncNumNonDraftPlans(currentUserId, tx) fails — typically a connection loss between statements, a constraint violation or missing row for currentUserId in the user-stats table, or SQL mismatch after schema changes.
Common situations: User row/statistics table missing or migrated (column renamed), DB connection pool exhausted mid-transaction, deadlock with another transaction touching the same user row, statement timeout on a busy table.
Related errors
- Error deleting org user: {err.Error()}
- error creating invite: %v
- error deleting invite: %v
- error accepting invite: %v
- error creating org user: %v
AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05).
Data as JSON: /api/errors/bc48aa9a0ee74169.
Report an issue: GitHub.