RAII & Dispose — Find the Bug¶
Category: Resource & Type-Safety Patterns — 12 buggy snippets across Go, Java, and Python where cleanup goes wrong.
12 buggy snippets across Go, Java, and Python.
Bug 1: Close Only on the Success Path (Go)¶
func read(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
data, err := io.ReadAll(f)
if err != nil {
return nil, err // BUG: f leaked here
}
f.Close()
return data, nil
}
Symptoms: Under read errors, file descriptors leak until the process exhausts them.
Find the bug
`f.Close()` runs only on the happy path. The error return between open and close skips it.Fix¶
f, err := os.Open(path)
if err != nil { return nil, err }
defer f.Close() // every path closes
return io.ReadAll(f)
Lesson¶
Bind release to the scope with defer, immediately after a successful acquire.
Bug 2: defer Inside a Loop (Go)¶
func processAll(paths []string) error {
for _, p := range paths {
f, err := os.Open(p)
if err != nil { return err }
defer f.Close() // BUG: deferred to FUNCTION end
process(f)
}
return nil
}
Symptoms: All files stay open until processAll returns. With many paths, "too many open files".
Find the bug
`defer` runs at function return, not per iteration. Every opened file accumulates.Fix¶
for _, p := range paths {
if err := func() error {
f, err := os.Open(p)
if err != nil { return err }
defer f.Close() // end of THIS iteration
return process(f)
}(); err != nil {
return err
}
}
Lesson¶
Give each iteration its own scope (a helper function) so defer fires per iteration.
Bug 3: Relying on __del__ for Cleanup (Python)¶
class Logger:
def __init__(self, path):
self.f = open(path, "a")
def __del__(self):
self.f.close() # BUG: may never run
def write(self, line):
self.f.write(line + "\n")
Symptoms: Logs not flushed; FDs leak; under reference cycles or os._exit, __del__ never runs.
Find the bug
`__del__` runs at GC time, which is non-deterministic and skippable. It is not cleanup.Fix¶
class Logger:
def __init__(self, path): self.f = open(path, "a")
def write(self, line): self.f.write(line + "\n")
def close(self): self.f.close()
def __enter__(self): return self
def __exit__(self, *exc): self.close(); return False
with Logger("app.log") as log:
log.write("started")
Lesson¶
Finalizers are a backstop, never primary cleanup. Use with/close.
Bug 4: defer Close() Before Checking the Error (Go)¶
f, err := os.Open(path)
defer f.Close() // BUG: f may be nil if err != nil
if err != nil {
return err
}
Symptoms: nil pointer dereference panic when os.Open fails.
Find the bug
`defer f.Close()` is registered before the error check; on failure `f` is nil and `Close()` panics.Fix¶
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // only after a successful open
Lesson¶
Acquire, check the error, then defer the release.
Bug 5: __exit__ Swallows the Exception (Python)¶
class Tx:
def __enter__(self): self.tx = db.begin(); return self.tx
def __exit__(self, exc_type, exc, tb):
self.tx.close()
return True # BUG: swallows every exception
Symptoms: Errors inside the with block vanish silently; the transaction is "successful" but the body failed.
Find the bug
Returning `True` from `__exit__` suppresses the body's exception. Callers never learn the operation failed.Fix¶
def __exit__(self, exc_type, exc, tb):
if exc_type is None:
self.tx.commit()
else:
self.tx.rollback()
self.tx.close()
return False # let the exception propagate
Lesson¶
__exit__ should return falsy unless suppression is genuinely intended.
Bug 6: Double Close Crashes (Java)¶
public void close() {
pool.release(conn); // BUG: not idempotent
}
// somewhere
lease.close();
// ... later, a finally also calls:
lease.close(); // releases the same conn twice → pool corruption
Symptoms: Connection returned to pool twice; another caller gets a conn that's also held elsewhere.
Find the bug
`close()` has no guard. A second call releases the same resource again.Fix¶
private boolean closed = false;
public void close() {
if (closed) return; // idempotent
closed = true;
pool.release(conn);
}
Lesson¶
Make close()/Dispose() idempotent; double-close must be harmless.
Bug 7: Dropping the Close Error on a Write (Go)¶
func save(path string, data []byte) error {
f, err := os.Create(path)
if err != nil { return err }
defer f.Close() // BUG: a flush error in Close() is discarded
_, err = f.Write(data)
return err
}
Symptoms: Write returns nil but the buffered flush during Close() fails — data silently lost; function reports success.
Find the bug
For writable files the final flush happens in `Close()`. A bare `defer f.Close()` throws that error away.Fix¶
func save(path string, data []byte) (err error) {
f, err := os.Create(path)
if err != nil { return err }
defer func() {
if cerr := f.Close(); cerr != nil && err == nil { err = cerr }
}()
_, err = f.Write(data)
return err
}
Lesson¶
On writes, capture and surface the Close()/flush error.
Bug 8: Lock Not Released on Early Return (Java)¶
void update(Account a, int amount) {
lock.lock();
if (amount <= 0) return; // BUG: returns while holding the lock → deadlock
a.credit(amount);
lock.unlock();
}
Symptoms: After one bad call, every future caller blocks forever.
Find the bug
The early `return` skips `lock.unlock()`. The lock is held permanently.Fix¶
void update(Account a, int amount) {
lock.lock();
try {
if (amount <= 0) return;
a.credit(amount);
} finally {
lock.unlock(); // always
}
}
Lesson¶
A lock must be released in a scope-bound way (try/finally or a lock guard), never on a single path.
Bug 9: Cleaner Action Captures this (Java)¶
public final class Buffer implements AutoCloseable {
private static final Cleaner CLEANER = Cleaner.create();
private final long handle;
public Buffer() {
this.handle = alloc();
CLEANER.register(this, () -> free(this.handle)); // BUG: lambda captures `this`
}
}
Symptoms: The Cleaner never fires; free is never called; native memory leaks.
Find the bug
The lambda captures `this`, so the registered action keeps the `Buffer` reachable forever — it never becomes collectable, so the Cleaner never runs.Fix¶
private static final class State implements Runnable {
final long handle;
State(long h) { this.handle = h; }
public void run() { free(handle); } // holds only the handle
}
public Buffer() {
long h = alloc();
CLEANER.register(this, new State(h)); // no `this` capture
}
Lesson¶
A finalizer/Cleaner action must not reference the object it cleans up.
Bug 10: Use-After-Close (Python)¶
def read_twice(path):
with open(path) as f:
first = f.read()
second = f.read() # BUG: f is closed here
return first, second
Symptoms: ValueError: I/O operation on closed file.
Find the bug
`f` is closed when the `with` block ends. The second `f.read()` touches a closed file.Fix¶
Lesson¶
Don't use a resource after its scope has closed it. Keep all use inside the with.
Bug 11: Ownership Transfer Closed by the Wrong Scope (Go)¶
func openDB() *sql.DB {
db, _ := sql.Open("postgres", dsn)
defer db.Close() // BUG: closes the DB before returning it
return db
}
Symptoms: The returned *sql.DB is already closed; every query fails with "database is closed".
Find the bug
The function *transfers ownership* by returning `db`, but its `defer` closes it at return — so the caller receives a dead handle.Fix¶
func openDB() (*sql.DB, error) {
return sql.Open("postgres", dsn) // caller owns it and closes it
}
// caller:
db, _ := openDB()
defer db.Close()
Lesson¶
A function that returns a resource transfers ownership and must not close it. The owner's scope closes it.
Bug 12: One finally Closing Two Resources (Java)¶
InputStream in = open(a);
OutputStream out = open(b);
try {
copy(in, out);
} finally {
in.close(); // BUG: if this throws, out.close() is skipped
out.close();
}
Symptoms: When in.close() throws, out leaks; and the original copy error can be masked.
Find the bug
A single `finally` closing both sequentially: a throw from the first close skips the second, leaking it.Fix¶
try (InputStream in = open(a);
OutputStream out = open(b)) {
copy(in, out);
} // both close LIFO; a close failure is suppressed onto the primary, not lost
Lesson¶
Use the language mechanism for multiple resources — it guarantees all close and preserves errors via suppression.
Practice Tips¶
- Run
go test -raceand watch FD/connection gauges to catch leaks. - Grep for
deferinsidefor— a frequent loop-leak. - Check that every
close()is idempotent and guards use-after-close. - Verify writable resources surface the flush/close error.
- Confirm no
__del__/finalizeis doing real work — backstop only. - Trace ownership: does any returned resource get closed by the creating scope?
← Tasks · Resource & Type-Safety · Roadmap · Next: Optimize
In this topic