Resource ownership and cleanup: close what you acquired

Follow a completed lease through Java try-with-resources and Python with, then separate deterministic cleanup from garbage collection. Failure and resource contracts · Object-oriented programming.

A booking screen takes a lock on a seat before it changes the seat. The lock is held by a server, which has no idea what happens inside your program. If the booking code fails halfway and nothing gives the lock back, nobody else can book that seat until the server gives up waiting. Someone has to be responsible for the release, whatever happens to the booking.

A completed acquisition needs an owner

A resource is something outside your program's own memory that has to be given back: a lease on a session, a lock token, a file, a socket. Acquiring it means the opening step finished and you now hold it. From that moment one scope owns it: the code that must release it, even when later code fails.

Memory works differently. Java and Python reclaim an object's memory once nothing can reach it, but the server that granted the lease cannot see your variables. It only learns the lease is finished when your code says so, usually by calling close(). Dropping the last variable that names a lease does not call it. (CPython happens to close an unreferenced plain file object, but that is an implementation detail you cannot count on for a lease; a with block is what guarantees it.)

SessionLease stray = openLease();
stray = null;                  // the reference is gone, the lease is not
try (SessionLease lease = openLease()) {
    reserveSeat();
}                              // lease.close() runs here

Running this with a close() that prints shows close once, for the second lease only. The first one stays open, and with its only variable gone nothing can close it.

Java: a dropped reference does not close anything

Read the steps as text

  1. SessionLease L3 will be opened before the block, outside it. The try-with-resources header will open SessionLease L4.
  2. Opening SessionLease L3 succeeds, and the variable stray holds it outside any try block.
  3. The variable stray is set to null, which drops the only reference to SessionLease L3. No code called close, so SessionLease L3 stays open and nothing can reach it to close it now.
  4. Opening SessionLease L4 succeeds, so the try block now owns it and must close it.
  5. The body finishes normally. The block now closes SessionLease L4, its only resource.
  6. SessionLease L4 closes normally. The block has nothing left to close. SessionLease L3 was never part of it and is still open.
  7. The try statement ends normally and nothing is thrown.

Java closes in reverse and keeps the first failure

A Java class that implements AutoCloseable can sit in a try-with-resources header. When the block ends, Java calls close() on every resource that finished opening, last opened first, whether the body returned or threw. The token was obtained through the lease, so it has to go first.

try (SessionLease lease = openLease();
     LockToken token = lease.lock()) {
    reserveSeat();             // throws SeatTaken
}

Suppose reserveSeat() throws SeatTaken and token.close() then throws TokenCloseFailure. Java closes the lease anyway. SeatTaken stays the primary exception, the one the caller receives, and TokenCloseFailure is attached to it in its suppressed list, readable with getSuppressed(). If the body had succeeded instead, a failing close would be the primary exception, and a later close failure would be suppressed behind it.

Java: close in reverse, keep the first failure

Read the steps as text

  1. The try-with-resources header will open SessionLease L7 and LockToken T2, in that order.
  2. Opening SessionLease L7 succeeds, so the try block now owns it and must close it.
  3. Opening LockToken T2 succeeds as well. The block now owns two open resources: SessionLease L7 and LockToken T2.
  4. The body throws SeatTaken, which is now the primary exception. The block still closes both resources, LockToken T2 first.
  5. LockToken T2 closes first and its close() throws TokenCloseFailure. Java keeps SeatTaken as the primary exception and adds TokenCloseFailure to its suppressed list. SessionLease L7 is next.
  6. SessionLease L7 closes second, normally, and SeatTaken keeps travelling as the primary exception. Closing is complete: LockToken T2, then SessionLease L7.
  7. The try statement ends and SeatTaken leaves it, carrying TokenCloseFailure in its suppressed list.

A failure while opening is a different case. If lease.lock() throws LockDenied, no token ever existed, so there is nothing to close. The body never runs, the lease that did open is closed, and LockDenied reaches the caller.

Java: only what finished opening gets closed

Read the steps as text

  1. The try-with-resources header will open SessionLease L5 and LockToken T9, in that order.
  2. Opening SessionLease L5 succeeds, so the try block now owns it and must close it.
  3. Opening LockToken T9 throws LockDenied, so that resource never exists. The body is skipped. SessionLease L5 is the only resource that opened, so it is the only one to close.
  4. SessionLease L5 closes normally, and LockDenied keeps travelling as the primary exception. Nothing else opened, so nothing else needs closing.
  5. The try statement ends and LockDenied leaves it.

Python hands the exception to each exit

Python's with statement uses a context manager: __enter__ runs before the body and __exit__(exc_type, exc, tb) after it, whether the body finished or raised. With several contexts, the last one entered exits first, and each __exit__ receives the exception that is active at that moment.

class SessionLease:
    def __enter__(self):
        return self
    def __exit__(self, exc_type, exc, tb):
        self.release()
        return False           # let any exception keep travelling

with SessionLease() as lease, LockToken(lease) as token:
    reserve_seat(lease)        # raises SeatTaken

The return value is a policy. A falsy value, False or None, lets the exception continue to the next exit and then to the caller. A truthy value swallows it: nothing is active any more and the statement after the with runs. Swallowing is right when the manager truly handles a known failure; returning True by habit hides bugs from every caller.

Python: __exit__ decides whether the exception survives

Read the steps as text

  1. The with statement will enter SessionLease L8 and LockToken T6, in that order.
  2. Entering SessionLease L8 succeeds, so the with statement now owns it and will call its __exit__.
  3. Entering LockToken T6 succeeds as well. Two contexts are entered: SessionLease L8 and LockToken T6.
  4. The body raises SeatTaken, which is now the active exception. Python still calls __exit__ on both entered contexts, LockToken T6 first.
  5. LockToken T6 exits first: __exit__ receives SeatTaken and returns False, so SeatTaken stays active. SessionLease L8 is next.
  6. SessionLease L8 exits second: __exit__ receives SeatTaken and returns True, which swallows it. Both exits have run.
  7. The with statement ends with no active exception, so the statement after it runs.

Python has no suppressed list. If __exit__ raises while another exception is active, the new exception replaces the active one. The replaced exception stays reachable as __context__, and the next __exit__ sees the new one. If __enter__ fails, that context was never entered, so its own __exit__ does not run, while earlier contexts still exit.

Choose the owner on purpose

The code that acquires a resource should normally be the code that releases it, in the same block. A function that receives a resource as an argument borrows it and does not close it unless its contract says it takes ownership. A class that creates a resource and keeps it in a field owns it, so that class needs its own close() or context manager and its caller becomes the next owner. A class that is handed one borrows it.

When you write a release, decide what it does with failure: let the body's exception stay primary, record a cleanup problem beside it, and swallow nothing without a reason you can state.

Recap

  • A completed acquisition creates an owner and a release path; dropping the last reference releases nothing.
  • Java try-with-resources closes in reverse, keeps the body's exception primary and records later close failures as suppressed.
  • Python with hands the active exception to each __exit__: a truthy return swallows it and a raising __exit__ replaces it.
  • A failed opening step means that resource, and only that resource, has nothing to close.
Go deeper

contextlib.contextmanager writes the same enter, body and exit shape as a generator with try and finally. Neither try-with-resources nor with makes close() safe to call twice: AutoCloseable only recommends it, Closeable requires it, and otherwise it is the class's own design choice. Java's Throwable.addSuppressed is the same mechanism try-with-resources uses, and you can call it yourself in a finally block.

Revisit exception contracts when a cleanup failure needs a public error vocabulary. Practise reverse-order cleanup in Stack Fundamentals: push each completed acquisition and pop the next owner when you attempt its release.