Transaction Without Rollback Path

ID

go.database_transactions_properly_handled_rollback

Severity

high

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Reliability

Language

Go

Tags

reliability, resource-leak

Description

Reports a function that opens a database transaction and commits it without ever providing a rollback path — no defer tx.Rollback(), and no tx.Rollback() call anywhere in the function.

The check is function-scoped and single-file. A begin call is reported only when the same function also commits: that is the corroboration that the Begin-named call really opened a transaction, so an unrelated Begin — on a parser, a span, a batch builder — never fires. It also exempts, by construction, the handover shape where the transaction is returned to the caller, since such a function does not commit and the caller owns the rollback.

Rationale

Between Begin and Commit, every statement can fail. When one does and the function returns the error, the transaction is neither committed nor rolled back: it stays open, holding its locks and its connection from the pool, until the driver eventually recycles the connection.

One leaked transaction is invisible. Under load they accumulate: the pool drains, unrelated queries start blocking on locks the abandoned transactions still hold, and the service stalls in a way that looks like a database problem rather than a missing line of Go.

func createUser(db *sql.DB, name string) error {
    tx, err := db.Begin() // FLAW — no rollback path anywhere in the function
    if err != nil {
        return err
    }

    if _, err := tx.Exec("insert into users(name) values (?)", name); err != nil {
        return err // transaction left open, holding locks
    }

    return tx.Commit()
}

Remediation

Defer the rollback immediately after a successful begin. Once the commit has succeeded the deferred rollback is a no-op, so the single line covers every error path, including a panic.

func createUser(db *sql.DB, name string) error {
    tx, err := db.Begin()
    if err != nil {
        return err
    }
    defer tx.Rollback() // no-op once Commit has succeeded

    if _, err := tx.Exec("insert into users(name) values (?)", name); err != nil {
        return err
    }

    return tx.Commit()
}

Configuration

Property Default Description

beginMethods

[Begin, BeginTx]

Method names that open a transaction. Add an ORM’s own name to have the rule follow it.

commitMethods

[Commit]

Method names that commit a transaction. A begin is reported only when the function also commits.

rollbackMethods

[Rollback]

Method names that abort a transaction. Any one of them, anywhere in the function, clears the finding.

© 2026 Xygeni. All rights reserved