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

  1. Use the iterator's remove() method to delete the current attribute instead of attributes.remove(key) inside the loop
  2. Collect keys to remove into a temporary list first, then remove them after iteration completes
  3. Iterate over a snapshot copy, e.g. new ArrayList<>(attributes.asList()), if mutation during the loop is unavoidable
  4. 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

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)