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

  1. Perform all executor work within the SqlSession's lifetime — get the transaction while the session is open
  2. If you need the Connection/Transaction, obtain it from the open SqlSession (sqlSession.getConnection()) inside the same scope
  3. Check executor.isClosed() before calling if lifecycle is uncertain
  4. 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

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


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