charmbracelet/crush · error
failed to begin transaction: %w
Error message
failed to begin transaction: %w
What it means
history.Service.createWithVersion opens a SQLite transaction via db.BeginTx before inserting a file version. This error wraps a BeginTx failure, meaning no transaction could even be started — the retry loop deliberately does not retry this case and returns immediately.
Source
Thrown at internal/history/file.go:95
// Get the latest version
latestFile := files[0] // Files are ordered by version DESC, created_at DESC
nextVersion := latestFile.Version + 1
return s.createWithVersion(ctx, sessionID, path, content, nextVersion)
}
func (s *service) createWithVersion(ctx context.Context, sessionID, path, content string, version int64) (File, error) {
// Maximum number of retries for transaction conflicts
const maxRetries = 3
var file File
var err error
// Retry loop for transaction conflicts
for attempt := range maxRetries {
// Start a transaction
tx, txErr := s.db.BeginTx(ctx, nil)
if txErr != nil {
return File{}, fmt.Errorf("failed to begin transaction: %w", txErr)
}
// Create a new queries instance with the transaction
qtx := s.q.WithTx(tx)
// Try to create the file within the transaction
dbFile, txErr := qtx.CreateFile(ctx, db.CreateFileParams{
ID: uuid.New().String(),
SessionID: sessionID,
Path: path,
Content: content,
Version: version,
})
if txErr != nil {
// Rollback the transaction
tx.Rollback()
// Check if this is a uniqueness constraint violationView on GitHub (pinned to 7944b8e522)
Solutions
- Check the wrapped error: `sql: database is closed` means DB lifetime management is wrong — keep the DB open until all writers finish
- If the context is canceled, propagate a fresh/longer-lived context for history writes
- Reduce concurrent long-lived transactions so the pool has a free connection for BeginTx
- Verify database connection options (MaxOpenConns, busy_timeout) are adequate for the write load
Example fix
// before file, err := hist.Create(ctx, sessionID, path, content) // after — use a detached context for durable history writes ctx2, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second) defer cancel() file, err := hist.Create(ctx2, sessionID, path, content)
Defensive patterns
Strategy: retry
Validate before calling
// Verify DB is usable before history writes
if err := s.db.PingContext(ctx); err != nil {
return fmt.Errorf("history db not ready: %w", err)
}
if err := ctx.Err(); err != nil {
return fmt.Errorf("context already done: %w", err)
} Type guard
func beginTxPossible(db *sql.DB, ctx context.Context) bool {
return db != nil && ctx.Err() == nil && db.PingContext(ctx) == nil
} Try / catch
file, err := hist.Create(ctx, sessionID, path, content)
if err != nil {
var retryable bool
if strings.Contains(err.Error(), "failed to begin transaction") &&
(errors.Is(err, context.Canceled) || strings.Contains(err.Error(), "too many clients")) {
retryable = true
}
if retryable {
file, err = hist.Create(ctx, sessionID, path, content) // single retry
}
if err != nil {
return err
}
} Prevention
- Keep the *sql.DB open for the lifetime of all writers; close only at shutdown
- Use context.WithoutCancel(ctx) (Go 1.21+) for durable history writes
- Set pragmatic pool limits (SetMaxOpenConns) and SQLite busy_timeout
- Ping the DB at startup and monitor for closed-connection errors during teardown
When it happens
Trigger: Calling Create or CreateVersion while the underlying database connection is closed, the pool is exhausted (all connections busy — e.g. a long-running transaction elsewhere holds the only connection), or the context passed in is already canceled.
Common situations: App shutdown closing the DB while a session still writes file history; too many concurrent writers blocking the single SQLite connection; passing a canceled/expired context to Create during a request timeout.
Related errors
- failed to commit transaction: %w
- error creating file history: %w
- error creating file history: %w
- failed to get session: %w
- failed to save todos: %w
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/06bf2e7a45e61cae.
Report an issue: GitHub.