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
- Commit before closing, exactly once: open -> work -> commit -> close
- In exception paths, close without committing (close(true) forces rollback); never commit in a finally after close
- With Spring, let the transaction manager own commit/rollback — remove manual sqlSession.commit() calls
- 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
- Structure every session as open -> work -> commit -> close, once
- Never commit in a finally block that can run after close
- With Spring, remove manual commit; the platform transaction manager owns it
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
- Executor was closed.
- A Cursor is already closed.
- Error accessing PooledConnection. Connection is invalid.
- Could not commit transaction. Cause: {}
- Parameter 'transactionFactory' must not be null
AI-assisted analysis of mybatis/mybatis-3@008069adb1 (2026-08-14).
Data as JSON: /api/errors/1cac298168b422cc.
Report an issue: GitHub.