{"record":{"id":"06bf2e7a45e61cae","repo":"charmbracelet/crush","slug":"failed-to-begin-transaction-w","errorCode":null,"errorMessage":"failed to begin transaction: %w","messagePattern":"failed to begin transaction: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/history/file.go","lineNumber":95,"sourceCode":"\t// Get the latest version\n\tlatestFile := files[0] // Files are ordered by version DESC, created_at DESC\n\tnextVersion := latestFile.Version + 1\n\n\treturn s.createWithVersion(ctx, sessionID, path, content, nextVersion)\n}\n\nfunc (s *service) createWithVersion(ctx context.Context, sessionID, path, content string, version int64) (File, error) {\n\t// Maximum number of retries for transaction conflicts\n\tconst maxRetries = 3\n\tvar file File\n\tvar err error\n\n\t// Retry loop for transaction conflicts\n\tfor attempt := range maxRetries {\n\t\t// Start a transaction\n\t\ttx, txErr := s.db.BeginTx(ctx, nil)\n\t\tif txErr != nil {\n\t\t\treturn File{}, fmt.Errorf(\"failed to begin transaction: %w\", txErr)\n\t\t}\n\n\t\t// Create a new queries instance with the transaction\n\t\tqtx := s.q.WithTx(tx)\n\n\t\t// Try to create the file within the transaction\n\t\tdbFile, txErr := qtx.CreateFile(ctx, db.CreateFileParams{\n\t\t\tID:        uuid.New().String(),\n\t\t\tSessionID: sessionID,\n\t\t\tPath:      path,\n\t\t\tContent:   content,\n\t\t\tVersion:   version,\n\t\t})\n\t\tif txErr != nil {\n\t\t\t// Rollback the transaction\n\t\t\ttx.Rollback()\n\n\t\t\t// Check if this is a uniqueness constraint violation","sourceCodeStart":77,"sourceCodeEnd":113,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/history/file.go#L77-L113","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nfile, err := hist.Create(ctx, sessionID, path, content)\n// after — use a detached context for durable history writes\nctx2, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)\ndefer cancel()\nfile, err := hist.Create(ctx2, sessionID, path, content)","handlingStrategy":"retry","validationCode":"// Verify DB is usable before history writes\nif err := s.db.PingContext(ctx); err != nil {\n    return fmt.Errorf(\"history db not ready: %w\", err)\n}\nif err := ctx.Err(); err != nil {\n    return fmt.Errorf(\"context already done: %w\", err)\n}","typeGuard":"func beginTxPossible(db *sql.DB, ctx context.Context) bool {\n    return db != nil && ctx.Err() == nil && db.PingContext(ctx) == nil\n}","tryCatchPattern":"file, err := hist.Create(ctx, sessionID, path, content)\nif err != nil {\n    var retryable bool\n    if strings.Contains(err.Error(), \"failed to begin transaction\") &&\n        (errors.Is(err, context.Canceled) || strings.Contains(err.Error(), \"too many clients\")) {\n        retryable = true\n    }\n    if retryable {\n        file, err = hist.Create(ctx, sessionID, path, content) // single retry\n    }\n    if err != nil {\n        return err\n    }\n}","preventionTips":["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"],"tags":["database","sqlite","transaction"],"backgroundTag":"begin-transaction-failed","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}