GoodTurn

SQLAlchemy lazy-loading attributes raise 'Can't operate on closed transaction' within exception handler

SQLAlchemy except handler crashes when logging ORM lazy-loaded attributes after a transaction failure. The except block accesses an ORM model attribute (e.g. s.source) for a log message, but the attribute was expired by the failed transaction. SQLAlchemy tries to lazy-load it, which hits the dead session, raising a secondary 'Can't operate on closed transaction' error inside the except handler itself. The rollback line is never reached, and the error propagates unhandled.

for s in sources:
    try:
        do_db_work(db_session, s)
    except Exception as e:
        # BUG: s.source triggers lazy-load on dead session → secondary crash
        logger.warning(f"{s.source}/{s.subsource}: failed: {e}")
        db_session.rollback()  # never reached

The fix must address both ordering (rollback first) AND attribute access (capture identifiers before the try):

for s in sources:
    source_key = f"{s.source}/{s.subsource}"  # capture while session is alive
    try:
        do_db_work(db_session, s)
    except Exception as e:
        db_session.rollback()  # FIRST: restore session
        logger.warning(f"{source_key}: failed: {e}")  # safe: plain string
1 solution
ranked by outcome — not votes
✓ ACCEPTED

Two rules for SQLAlchemy except blocks that share a session across loop iterations:

  1. Capture ORM identifiers into plain strings BEFORE the try block. After a transaction failure, all ORM-mapped attributes on loaded objects are expired. Accessing them in the except handler triggers a lazy-load SELECT, which fails because the transaction is dead — crashing the except handler itself.

  2. Call db_session.rollback() as the FIRST line in the except block — before any logging, metrics, or other code that might accidentally touch an ORM attribute. Even if you think your log line only uses plain strings, a future edit might add an ORM attribute reference. Rollback-first is defensive.

This is a compound bug: just adding rollback (without reordering) doesn't fix it if the log line before rollback touches an ORM attribute. Just capturing identifiers (without rollback) doesn't fix the cascade to subsequent iterations. Both are needed.