Close Resources and Check Deferred Close Errors in Go

Catches unclosed response bodies and sql.Rows, missing rows.Err checks, defer inside loops, and ignored Close errors on files opened for writing.

What it flags

Flaggedexports/emails.go
func ExportEmails(ctx context.Context, db *sql.DB, path string) error {
	rows, err := db.QueryContext(ctx, "SELECT email FROM users ORDER BY id")
	if err != nil {
		return fmt.Errorf("query users: %w", err)
	}

	f, err := os.Create(path)
	if err != nil {
		return fmt.Errorf("create export: %w", err)
	}
	defer f.Close()

	for rows.Next() {
		var email string
		if err := rows.Scan(&email); err != nil {
			return fmt.Errorf("scan user: %w", err)
		}
		if _, err := fmt.Fprintln(f, email); err != nil {
			return fmt.Errorf("write export: %w", err)
		}
	}
	return nil // skips rows.Err: a dropped connection looks like a complete export
}
Passesexports/emails.go
func ExportEmails(ctx context.Context, db *sql.DB, path string) (err error) {
	rows, err := db.QueryContext(ctx, "SELECT email FROM users ORDER BY id")
	if err != nil {
		return fmt.Errorf("query users: %w", err)
	}
	defer rows.Close()

	f, err := os.Create(path)
	if err != nil {
		return fmt.Errorf("create export: %w", err)
	}
	defer func() { err = errors.Join(err, f.Close()) }()

	for rows.Next() {
		var email string
		if err := rows.Scan(&email); err != nil {
			return fmt.Errorf("scan user: %w", err)
		}
		if _, err := fmt.Fprintln(f, email); err != nil {
			return fmt.Errorf("write export: %w", err)
		}
	}
	return rows.Err()
}

Why it matters

An unclosed HTTP response body leaks its connection, and sql.Rows left open after an early return keeps a pool connection checked out until queries start waiting for one. A missing rows.Err check turns a connection dropped mid-query into a truncated result that looks complete. The error from Close on a written file, gzip.Writer, or zip.Writer can be the only signal that the final bytes were never written, and a bare defer f.Close() throws it away.

Agent instructions

cubic checks every pull request against these instructions. Paste them into AGENTS.md or Cursor rules and your AI coding tool follows them while it writes code.

AGENTS.md

Add this section to AGENTS.md at the repository root. Codex, Cursor, and other AI coding tools read it.

## Close Resources and Check Deferred Close Errors in Go

Applies to `**/*.go`.

Review Go code that opens connections, files, and writers for resources that are not released and errors lost at close.

Flag changes that:
- Get an `*http.Response` without closing `resp.Body` on every path, including early returns on a non-2xx status placed before `defer resp.Body.Close()`.
- Call `Query` or `QueryContext` without `defer rows.Close()` right after the error check, so an early return before or inside the loop keeps a pool connection checked out.
- Loop over `rows.Next()` without checking `rows.Err()` afterward, so a driver or network error mid-iteration looks like a shorter result.
- Put `defer` inside a `for` loop to close files, rows, or response bodies, or to unlock a mutex, so every iteration holds its resource until the function returns.
- Ignore the error from `Close` on files and writers opened for writing (`os.Create`, `os.OpenFile` with write flags, `gzip.Writer`, `zip.Writer`), from `bufio.Writer.Flush`, or from `csv.Writer.Error()` after `Flush`.
- Begin a `sql.Tx` without `defer tx.Rollback()` or an explicit rollback on every error path.

Do not flag:
- Ignored errors from `resp.Body.Close()` or from `Close` on files opened read-only with `os.Open`.
- `defer tx.Rollback()` followed by `tx.Commit()`; the deferred rollback returns `sql.ErrTxDone` after a commit and is harmless.
- Resources returned to the caller when the function contract makes the caller responsible for closing them.

Checks files matching **/*.go.

More Go agents

See all Go code review standards

Run this agent on every pull request

cubic reviews every pull request against the agents you turn on and comments on the lines that break them.

Catch this in your code