jhy/jsoup · error · ConcurrentModificationException
Use Iterator#remove() instead to remove attributes while…
Error message
Use Iterator#remove() instead to remove attributes while iterating.
What it means
jsoup's Attributes iterator is fail-fast: it captures the attribute count when created and throws ConcurrentModificationException if the map's size changes during iteration by any means other than the iterator's own remove() method. This prevents undefined behavior while iterating the internal arrays.
Solutions
- Use the iterator's remove() method to delete the current attribute instead of attributes.remove(key) inside the loop
- Collect keys to remove into a temporary list first, then remove them after iteration completes
- Iterate over a snapshot copy, e.g. new ArrayList<>(attributes.asList()), if mutation during the loop is unavoidable
- Synchronize or restructure concurrent access so attributes are not modified while another thread iterates
Example fix
// before
for (Attribute a : node.attributes()) {
if (a.getKey().startsWith("data-")) node.attributes().remove(a.getKey()); // CME
}
// after
Iterator<Attribute> it = node.attributes().iterator();
while (it.hasNext()) {
if (it.next().getKey().startsWith("data-")) it.remove();
} Defensive patterns
Strategy: type-guard
Validate before calling
// remove after iteration
List<String> toRemove = new ArrayList<>();
for (Attribute a : node.attributes()) if (a.getKey().startsWith("tmp-")) toRemove.add(a.getKey());
toRemove.forEach(k -> node.attributes().remove(k)); Type guard
// iterate a snapshot copy instead of the live map List<Attribute> snapshot = new ArrayList<>(node.attributes().asList());
Try / catch
try { for (Attribute a : node.attributes()) { ... } } catch (ConcurrentModificationException e) { /* switch to iterator.remove() or snapshot iteration */ } Prevention
- Use iterator.remove() for removal during iteration
- Snapshot attributes (asList copy) when mutation is possible
- Avoid mutating shared nodes from multiple threads
- Keep removal logic outside of traversal loops where possible
When it happens
Trigger: Calling Attributes.remove(key), size-changing operations, or modifying attributes on the owning Element while looping over attributes() iterator or an enhanced for loop, instead of using iterator.remove().
Common situations: Removing attributes inside a for (Attribute a : element.attributes()) loop; clearing attributes while streaming over them; concurrent code mutating an element's attributes during traversal.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
AI-assisted analysis of jhy/jsoup@9851ac5d9c (2026-09-08).
Data as JSON: /api/errors/728ee7a5a9f69eee.
Report an issue: GitHub.
Appendix: source
Thrown at src/main/java/org/jsoup/nodes/Attributes.java:502
break;
}
return i < size;
}
@Override
public Attribute next() {
checkModified();
if (i >= size) throw new NoSuchElementException();
String key = keys[i];
assert key != null;
final Attribute attr = new Attribute(key, (String) vals[i], Attributes.this);
i++;
return attr;
}
private void checkModified() {
if (size != expectedSize) throw new ConcurrentModificationException("Use Iterator#remove() instead to remove attributes while iterating.");
}
@Override
public void remove() {
Attributes.this.remove(--i); // next() advanced, so rewind
expectedSize--;
}
};
}
/**
Get the attributes as a List, for iteration.
@return a view of the attributes as an unmodifiable List.
*/
public List<Attribute> asList() {
ArrayList<Attribute> list = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
String key = keys[i];View on GitHub (pinned to 9851ac5d9c)