Every resource has exactly one owner responsible for releasing it, and the release point is tied to scope, not to programmer discipline. Languages differ only in who enforces this: Rust checks ownership in the compiler; the rest provide a construct (try-with-resources, defer, with) and catch what was forgotten with a linter. A garbage collector does not fill this role: it manages memory, not file handles, connections, and locks, and the moment of finalization is undefined.
The idiom has a name from C++: Bjarne Stroustrup’s “resource acquisition is initialization” (RAII) — acquire in a constructor, release in the destructor, and let scope exit, including exit by exception, run the release. Rust turned the idiom into a checked rule: every value has one owner, ownership moves explicitly, borrows are tracked by the compiler, and Drop runs when the owner goes out of scope. Java’s try-with-resources, C#’s using, Go’s defer and Python’s with give the same release point without the compiler proof.
// the reference: ownership in the type; the compiler proves no use-after-free
fn process(conn: Connection) -> Report { ... }
// took ownership: the caller can no longer use conn// try-with-resources + AutoCloseable; Cleaner for native resources
try (var conn = pool.acquire(); var tx = conn.begin()) {
...
} // closed in reverse order, guaranteed// defer right after acquire is the idiom; a linter catches a forgotten defer
conn, err := pool.Acquire(ctx)
if err != nil { return err }
defer conn.Release()# context manager; ExitStack for several resources
with pool.acquire() as conn, conn.begin() as tx:
...How it fails in a service. A handler takes a connection from the pool, an exception skips the release, and the pool leaks one connection per error; under an error spike the pool empties and the service stops serving requests that have nothing to do with the original bug. A transaction that is never closed is worse: in PostgreSQL a session left “idle in transaction” keeps its snapshot, holds back vacuum and can keep row locks. Both are scope bugs, and both disappear when the release is tied to the block rather than to the happy path.
Where scope is the wrong shape. Some lifetimes are not lexical: a connection handed to a background task, a lease renewed across requests, a pool that lives as long as the process. Then ownership has to be transferred explicitly and the new owner named, not left to whoever last touched it. Traps of the idioms themselves: Go’s defer inside a loop runs at function exit, not at the end of the iteration, so a loop over files holds every descriptor until it returns; finalizers and Java’s Cleaner are a safety net for a forgotten release, never the release mechanism.