mybatis/mybatis-3 · error · ExecutorException

Cannot commit, transaction is already closed

Error message

Cannot commit, transaction is already closed

What it means

BaseExecutor.commit(required) throws ExecutorException 'Cannot commit, transaction is already closed' when the executor's closed flag is set. Committing flushes the local cache and statements then delegates to the Transaction; after close(), the transaction and statements are released, so commit is rejected. Note this fires even for commit(false), and note that rollback(boolean) deliberately does NOT throw when closed.

Source

Thrown at src/main/java/org/apache/ibatis/executor/BaseExecutor.java:253

        cacheKey.update(value);
      }
    }
    if (configuration.getEnvironment() != null) {
      // issue #176
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }

  @Override
  public boolean isCached(MappedStatement ms, CacheKey key) {
    return localCache.getObject(key) != null;
  }

  @Override
  public void commit(boolean required) throws SQLException {
    if (closed) {
      throw new ExecutorException("Cannot commit, transaction is already closed");
    }
    clearLocalCache();
    flushStatements();
    if (required) {
      transaction.commit();
    }
  }

  @Override
  public void rollback(boolean required) throws SQLException {
    if (!closed) {
      try {
        clearLocalCache();
        flushStatements(true);
      } finally {
        if (required) {
          transaction.rollback();
        }

View on GitHub (pinned to 008069adb1)

Solutions

  1. Commit before closing, exactly once: open -> work -> commit -> close
  2. In exception paths, close without committing (close(true) forces rollback); never commit in a finally after close
  3. With Spring, let the transaction manager own commit/rollback — remove manual sqlSession.commit() calls
  4. Guard with !session.getConnection()... isClosed checks or track session state if lifecycle is complex

Example fix

// before
try (SqlSession s = factory.openSession()) {
  s.update(...);
} catch (Exception e) {
  ...
} finally {
  session.commit(); // session already closed by try-with-resources
}

// after
try (SqlSession s = factory.openSession()) {
  s.update(...);
  s.commit(); // commit while open
}
Defensive patterns

Strategy: validation

Validate before calling

// Commit exactly once, before close:
try (SqlSession s = factory.openSession()) {
  s.update("...", obj);
  s.commit(); // inside the scope
} // close happens after commit

Try / catch

try {
  sqlSession.commit();
} catch (ExecutorException e) {
  if ("Cannot commit, transaction is already closed".equals(e.getMessage())) {
    // session already closed and its transaction finalized: nothing to do; log and continue
  } else throw e;
}

Prevention

When it happens

Trigger: Calling sqlSession.commit() after sqlSession.close(); double-commit where a framework (Spring tx manager) commits and manual code commits again on the closed executor; commit inside a finally block that runs after an earlier close; commit on a session whose executor was closed by close(forceRollback).

Common situations: finally { session.commit(); } after an exception path already closed the session; mixing Spring transaction management with manual commit; session-per-request code where close happens in a filter before an interceptor commits.

Related errors


AI-assisted analysis of mybatis/mybatis-3@008069adb1 (2026-08-14). Data as JSON: /api/errors/1cac298168b422cc. Report an issue: GitHub.