How do I manage transactions with try-with-resources in EK9?

← Dependency Injection · Ref: Q333

The simplest and most recommended transaction pattern uses try-with-resources with a type implementing the Transaction trait. Transaction extends Closeable, so operator close provides automatic cleanup.

BASIC PATTERN

Create a transaction in the try header, use it in the body, commit on success:

  try
    -> txn <- DatabaseTransaction(connection)
    updateRecords(txn)
    txn.commit()
  catch
    -> ex as Exception
    handleError(ex)

When the try scope exits, operator close is called automatically.

AUTO-ROLLBACK SAFETY NET

The operator close implementation can check isCommitted() and rollback if the transaction was not committed. This prevents partial commits when exceptions occur between operations.

WHY THIS PATTERN IS BEST

1. TRANSACTION SCOPE IS VISIBLE: the try block clearly delimits transaction boundaries
2. AUTOMATIC CLEANUP: operator close is called regardless of how the scope exits
3. EXCEPTION SAFE: exceptions trigger automatic cleanup, no finally block needed
4. AI FRIENDLY: AI can see the transaction scope and generate correct code
5. SIMILAR TO RUST: mirrors Diesel's RAII pattern and Go's defer

COMPARISON WITH SPRING

Spring @Transactional hides the transaction boundary. Self-invocation bypasses the proxy. Checked exceptions silently commit. EK9's try-with-resources has none of these failure modes.

See Q137 for try-with-resources basics. See Q134 for try/catch patterns. See Q334 for delegation pattern. See Q335 for callback pattern. See Q338 for partial commit prevention.

Example

defines module qa.di.transaction.try.resources

  defines trait

    <?-
      Connection abstraction for the example.
    -?>
    Connection
      execute()
        -> statement as String
        <- result as String?

  defines class

    <?-
      A concrete transaction that tracks commit state.
      Implements Transaction trait (which extends Closeable).
    -?>
    DatabaseTransaction with trait of Transaction
      connectionName <- String()
      committed <- false

      DatabaseTransaction()
        -> connectionName as String
        this.connectionName: connectionName

      executeUpdate()
        -> statement as String
        <- result as String: `${connectionName}: ${statement}`

      override commit()
        committed: true

      override rollback()
        committed: false

      override isCommitted() as pure
        <- rtn as Boolean: committed

      override operator close as pure
        stdout <- Stdout()
        stdout.println("Close: committed=" + $committed)

      override operator ? as pure
        <- rtn <- true

  defines program

    TransactionTryResourcesDemo()
      stdout <- Stdout()

      // === TRY-WITH-RESOURCES: most explicit pattern ===

      stdout.println("=== Successful transaction ===")
      try
        -> txn <- DatabaseTransaction("primary-db")
        result1 <- txn.executeUpdate("UPDATE accounts SET balance = 100")
        stdout.println(result1)
        result2 <- txn.executeUpdate("INSERT INTO audit_log VALUES ('transfer')")
        stdout.println(result2)
        txn.commit()
      catch
        -> ex as Exception
        stdout.println("Error: " + $ex)

      // === UNCOMMITTED TRANSACTION: close called without commit ===

      stdout.println("=== Uncommitted transaction ===")
      try
        -> txn <- DatabaseTransaction("secondary-db")
        result3 <- txn.executeUpdate("UPDATE inventory SET count = 0")
        stdout.println(result3)
        // No commit — operator close sees committed=false
      catch
        -> ex as Exception
        stdout.println("Error: " + $ex)

Common mistakes

E08180 — Class fields must be initialized inline at declaration. Declaring a field without initialization produces a field-not-initialized error unless the field is an injection point ('!'). See ek9 -h E08180 for details.

Incorrect:

connectionName as String

Correct:

connectionName <- String()
Other ways to ask this
  • How do I use the Transaction trait with try-with-resources?
  • What is the simplest transaction pattern in EK9?
  • How does operator close work with transactions?

Coming from another language?

Java Spring: @Transactional annotation with proxy-based AOP, seven silent failure modes. Go: defer tx.Rollback() after tx, err := db.Begin(). Rust: conn.transaction(|txn| { ... }) RAII closure. C#: using (var scope = new TransactionScope()) { ... scope.Complete(); }. Python: with conn.begin() as txn: .... EK9: try -> txn <- Transaction() with operator close for auto-cleanup, isCommitted() check in close for safety.

Keywords: safety, rollback, migrate, try, transaction, dependency, cleanup, commit, inject, close, automatic, resources, scope