mybatis/mybatis-3 · error · IllegalStateException
Cannot open more than one iterator on a Cursor
Error message
Cannot open more than one iterator on a Cursor
What it means
A MyBatis Cursor (streaming result iterator returned by SqlSession.selectCursor) allows only ONE Iterator to be opened during its lifetime. The Cursor class implements Iterable by returning its internal single iterator, and sets iteratorRetrieved=true on the first call. Calling iterator() again (directly, via for-each, or via forEach) throws IllegalStateException because two concurrent iterators would share and corrupt the same underlying ResultSet.
Source
Thrown at src/main/java/org/apache/ibatis/cursor/defaults/DefaultCursor.java:97
@Override
public boolean isOpen() {
return status == CursorStatus.OPEN;
}
@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();View on GitHub (pinned to 008069adb1)
Solutions
- Fetch a new Cursor via a fresh sqlSession.selectCursor(...) call for each iteration pass
- Materialize the results into a List once (e.g., new ArrayList<>(cursor) or cursor.stream().collect(toList())) if you need multiple passes
- Track iteration yourself: only call iterator() once per Cursor instance and consume it fully (or close it) before discarding
- If you need resumable streaming, use getCurrentIndex() to record position and re-query with a RowBounds offset from a new Cursor
Example fix
// before
try (Cursor<User> c = sqlSession.selectCursor("findUsers")) {
c.forEach(u -> log.debug(u)); // iterator #1
for (User u : c) process(u); // iterator #2 -> IllegalStateException
}
// after
try (Cursor<User> c = sqlSession.selectCursor("findUsers")) {
List<User> users = new ArrayList<>();
c.forEach(users::add); // single iteration
users.forEach(u -> log.debug(u));
users.forEach(this::process);
} Defensive patterns
Strategy: validation
Validate before calling
// DefaultCursor tracks retrieval internally but exposes no getter;
// wrap it so iteration happens exactly once per Cursor.
boolean iterated = false;
try (Cursor<User> c = sqlSession.selectCursor("findUsers")) {
if (iterated) throw new IllegalStateException("cursor already iterated");
iterated = true;
c.forEach(this::process);
} Try / catch
// Only if a stray second iteration is possible:
try {
for (User u : cursor) { ... }
} catch (IllegalStateException e) {
if (e.getMessage().contains("more than one iterator")) {
// re-open: fetch a fresh Cursor from a new selectCursor call
} else throw e;
} Prevention
- Treat a Cursor as single-shot: one iterator per instance, then close
- Materialize to a List when you need multiple passes or to hand data across layers
- Never pass a Cursor to utility code that iterates Iterables without tracking consumption
When it happens
Trigger: Calling cursor.iterator() a second time; using the same Cursor object in two enhanced for-loops; calling cursor.forEach(x -> ...) after already iterating it with for-each; passing the Cursor to a method that iterates it after your code already did; re-streaming a partially consumed Cursor in a retry loop.
Common situations: Streaming large result sets with selectCursor and accidentally iterating twice (e.g., once for logging/counting, once for processing); helper utilities that accept Iterable and iterate internally; retry logic that re-consumes the same Cursor; Spring code that wraps the Cursor in a Stream and also loops over it.
Related errors
- Cannot remove element from Cursor
- A Cursor is already closed.
- Cursor results cannot be mapped to multiple resultMaps
- Mapped Statements with nested result mappings cannot be safe
- Missing resultMap in property '{}'. Parameters of type java
AI-assisted analysis of mybatis/mybatis-3@008069adb1 (2026-08-14).
Data as JSON: /api/errors/1c64f051b354e2e4.
Report an issue: GitHub.