mybatis/mybatis-3 · error · ExecutorException
Executor was closed.
Error message
Executor was closed.
What it means
BaseExecutor guards every public operation with a 'closed' flag set by close(boolean). getTransaction() throws ExecutorException 'Executor was closed.' if called after the executor was closed. Executors are closed when the owning SqlSession closes (or a Spring-managed session ends), so this usually means the SqlSession lifecycle ended before the code touched getTransaction().
Source
Thrown at src/main/java/org/apache/ibatis/executor/BaseExecutor.java:79
protected Configuration configuration;
protected int queryStack;
private boolean closed;
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<>();
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
@Override
public Transaction getTransaction() {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
return transaction;
}
@Override
public void close(boolean forceRollback) {
try {
try {
rollback(forceRollback);
} finally {
if (transaction != null) {
transaction.close();
}
}
} catch (SQLException e) {
// Ignore. There's nothing that can be done at this point.
log.warn("Unexpected exception on closing transaction. Cause: " + e);
} finally {View on GitHub (pinned to 008069adb1)
Solutions
- Perform all executor work within the SqlSession's lifetime — get the transaction while the session is open
- If you need the Connection/Transaction, obtain it from the open SqlSession (sqlSession.getConnection()) inside the same scope
- Check executor.isClosed() before calling if lifecycle is uncertain
- Fix lazy-loading configuration so deferred loads resolve before session close (aggressiveLazyLoading or fetching eagerly)
Example fix
// before
Executor exec = session.getConfiguration()... ; // retained reference
session.close();
Transaction t = exec.getTransaction(); // ExecutorException
// after
try (SqlSession session = factory.openSession()) {
Connection c = session.getConnection(); // safe: session open
// work...
} Defensive patterns
Strategy: validation
Validate before calling
if (!executor.isClosed()) {
Transaction tx = executor.getTransaction();
} Try / catch
try {
return executor.getTransaction();
} catch (ExecutorException e) {
if ("Executor was closed.".equals(e.getMessage())) {
// session ended: open a new SqlSession for further work
} else throw e;
} Prevention
- Never retain an Executor beyond its SqlSession's lifetime
- Use sqlSession.getConnection() inside the open scope instead of touching the executor's transaction
- Resolve lazy loads before the session closes
When it happens
Trigger: Calling executor.getTransaction() (directly, or via a plugin/framework that inspects the executor) after sqlSession.close(); keeping an Executor reference beyond its session's lifetime; accessing the executor from a lazily-run callback (lazy loading, async task) after the session closed.
Common situations: Custom MyBatis plugins/interceptors that grab the executor and use it later; storing an Executor in a field or cache; lazy-loading triggers firing after the session was closed; multi-threaded use where one thread closes while another reads the transaction.
Related errors
- Cannot commit, transaction is already closed
- A Cursor is already closed.
- Error accessing PooledConnection. Connection is invalid.
- Unknown execution method for: {name}
- Mapper method '{name}' attempted to return null from a metho
AI-assisted analysis of mybatis/mybatis-3@008069adb1 (2026-08-14).
Data as JSON: /api/errors/eb6fc57de6305fad.
Report an issue: GitHub.