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) {
// ignoreView on GitHub (pinned to 008069adb1)
Solutions
- 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)
- If the consumer is in another layer, return List<T> instead of Cursor<T>, or materialize with a collector before closing
- Check cursor.isOpen() before calling iterator() to fail gracefully
- 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
- Consume the Cursor inside the same try-with-resources block that opened the SqlSession
- Do not return a Cursor from a scope that closes the session before consumption
- Check isOpen() before iterating when ownership crosses method boundaries
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
- Cannot open more than one iterator on a Cursor
- Cannot remove element from Cursor
- Error accessing PooledConnection. Connection is invalid.
- PooledDataSource: Unknown severe error condition. The conne
- Executor was closed.
AI-assisted analysis of mybatis/mybatis-3@008069adb1 (2026-08-14).
Data as JSON: /api/errors/c49bfb444c79e887.
Report an issue: GitHub.