plandex-ai/plandex · error

error starting transaction: %v

Error message

error starting transaction: %v

What it means

lockRepoDB opens a REPEATABLE READ transaction with Conn.BeginTxx. If the driver cannot start the transaction (database unreachable, too many connections, context already cancelled/expired, pool exhausted), the error is wrapped as "error starting transaction: %v".

Source

Thrown at app/server/db/locks.go:111

	cancelFn := params.CancelFn

	if orgId == "" {
		return "", fmt.Errorf("orgId is required")
	}
	if planId == "" {
		return "", fmt.Errorf("planId is required")
	}
	if scope != LockScopeRead && scope != LockScopeWrite {
		return "", fmt.Errorf("invalid lock scope: %s", scope)
	}

	tx, err := Conn.BeginTxx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead})
	if err != nil {
		if locksVerboseLogging {
			log.Printf("[Lock][%d] Error starting transaction %v | reason: %s",
				goroutineID, err, params.Reason)
		}
		return "", fmt.Errorf("error starting transaction: %v", err)
	}

	var committed bool

	// Ensure that rollback is attempted in case of failure
	defer func() {
		if committed {
			return
		}

		panicErr := recover()
		if panicErr != nil {
			log.Printf("panic in lock repo: %v", panicErr)
		}

		if rbErr := tx.Rollback(); rbErr != nil {
			if rbErr == sql.ErrTxDone {
				if locksVerboseLogging {

View on GitHub (pinned to e2d772072e)

Solutions

  1. Read the wrapped driver error — "too many connections" means raise the pool size or reduce concurrency; "context canceled/deadline exceeded" means fix the caller's timeout.
  2. Verify database connectivity and DSN (host, port, credentials, max_connections) with a simple ping.
  3. Retry with backoff — the lock layer already retries; a persistent failure here indicates infra, not lock contention.
  4. Ensure the context passed in LockRepoParams.Ctx outlives the lock attempt.

Example fix

// before
tx, err := Conn.BeginTxx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead})
// after (caller side)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
lockId, err := db.LockRepo(ctx, cancelFn, params) // retryable on transient errors
Defensive patterns

Strategy: retry

Validate before calling

// before locking
if err := db.Conn.PingContext(ctx); err != nil {
    return fmt.Errorf("database unreachable: %w", err)
}
if ctx.Err() != nil { return ctx.Err() }

Type guard

func ctxUsable(ctx context.Context) bool {
    return ctx != nil && ctx.Err() == nil
}

Try / catch

var lockId string
var err error
for i := 0; i < 3; i++ {
    lockId, err = db.LockRepo(ctx, cancel, params)
    if err == nil || !isTransientDBErr(err) { break }
    time.Sleep(time.Duration(1<<i) * 200 * time.Millisecond)
}

Prevention

When it happens

Trigger: Postgres down or restarting; connection pool exhausted (many concurrent lock attempts); params.Ctx cancelled or timed out mid-flight so BeginTxx aborts; bad DSN/credentials.

Common situations: Load spike causing "too many clients already"; network partition to the DB; long-running request whose context deadline expired before locking; misconfigured DATABASE_URL after a deploy.

Related errors


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