mybatis/mybatis-3 · error · IllegalStateException

A Cursor is already closed.

Error message

A Cursor is already closed.

What it means

MyBatis DefaultCursor throws IllegalStateException when iterator() is called after the Cursor has been closed. Closing releases the underlying ResultSet (and on some configurations the Statement), so iteration is impossible afterwards. The closed flag is set by close(), by try-with-resources exit, or implicitly when the cursor is fully consumed and the session closes it.

Source

Thrown at src/main/java/org/apache/ibatis/cursor/defaults/DefaultCursor.java:100

  }

  @Override
  public boolean isConsumed() {
    return status == CursorStatus.CONSUMED;
  }

  @Override
  public int getCurrentIndex() {
    return rowBounds.getOffset() + cursorIterator.iteratorIndex;
  }

  @Override
  public Iterator<T> iterator() {
    if (iteratorRetrieved) {
      throw new IllegalStateException("Cannot open more than one iterator on a Cursor");
    }
    if (isClosed()) {
      throw new IllegalStateException("A Cursor is already closed.");
    }
    iteratorRetrieved = true;
    return cursorIterator;
  }

  @Override
  public void close() {
    if (isClosed()) {
      return;
    }

    ResultSet rs = rsw.getResultSet();
    try {
      if (rs != null) {
        rs.close();
      }
    } catch (SQLException e) {
      // ignore

View on GitHub (pinned to 008069adb1)

Solutions

  1. Keep the SqlSession and Cursor open while consuming: create the Cursor and iterate inside the same scope (same try-with-resources or same open session)
  2. If the consumer is in another layer, return List<T> instead of Cursor<T>, or materialize with a collector before closing
  3. Check cursor.isOpen() before calling iterator() to fail gracefully
  4. With Spring, use SqlSessionFactoryBean cursors within a @Transactional method or a SqlSessionTemplate.execute with CursorCallback handled in one block

Example fix

// before
public Cursor<User> users() {
  try (SqlSession s = factory.openSession()) {
    return s.selectCursor("findUsers"); // session closes -> cursor closed
  }
}

// after
public List<User> users() {
  try (SqlSession s = factory.openSession();
       Cursor<User> c = s.selectCursor("findUsers")) {
    List<User> out = new ArrayList<>();
    c.forEach(out::add);
    return out;
  }
}
Defensive patterns

Strategy: validation

Validate before calling

if (cursor.isOpen()) {
  Iterator<User> it = cursor.iterator(); // safe
} else {
  // cursor closed: fetch a new one via sqlSession.selectCursor(...)
}

Try / catch

try {
  Iterator<User> it = cursor.iterator();
} catch (IllegalStateException e) {
  if (e.getMessage().contains("already closed")) {
    // reopen session/cursor or fall back to selectList
  } else throw e;
}

Prevention

When it happens

Trigger: Calling iterator() outside a try-with-resources block after close() ran; returning a Cursor from a method that closes the SqlSession (or the try block) before the caller iterates; iterating a Cursor whose SqlSession was already closed/committed; using a Cursor after fully consuming it in a prior pass that also closed it.

Common situations: Returning Cursor<T> from a DAO where the SqlSession is closed by a template/interceptor before the caller consumes it; try-with-resources scoping mistake where the Cursor is created inside the resource block but consumed after it; mixing selectCursor with Spring's SqlSessionTemplate which closes the session on method return.

Related errors


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