jhy/jsoup · error · Selector.SelectorParseException

Could not parse nth-index

Error message

Could not parse nth-index '%s': unexpected format

What it means

The argument to :nth-child(), :nth-last-child(), :nth-of-type() or :nth-last-of-type() did not match any of jsoup's accepted forms (odd/even/an+b, plain integers, or +n offsets), so cssNthChild threw Selector.SelectorParseException. The nth expression grammar is strict: whitespace-free an+b patterns or simple integers.

Solutions

  1. Rewrite the nth expression in canonical form: odd, even, an integer, or 'an+b' like 2n+1 (no spaces)
  2. Verify the interpolated variable actually contains a valid nth expression before building the selector
  3. Catch Selector.SelectorParseException around select() to surface the bad argument to users
  4. Compute the element by index in Java (children().get(i)) if the pattern is too complex

Example fix

// before
Elements els = doc.select("li:nth-child(2 n + 1)");
// after
Elements els = doc.select("li:nth-child(2n+1)");
Defensive patterns

Strategy: validation

Validate before calling

if (!arg.matches("(odd|even|\\d+|[+-]?\\d*n([+-]\\d+)?|\\d+n)")) {
    throw new IllegalArgumentException("Invalid nth expression: " + arg);
}

Type guard

boolean isValidNthArg(String arg) {
    return arg != null && arg.trim().matches("(odd|even|[+-]?\\d*n([+-]\\d+)?|\\d+)");
}

Try / catch

try {
    Elements els = doc.select("li:nth-child(" + arg + ")");
} catch (Selector.SelectorParseException e) {
    if (e.getMessage().contains("nth-index")) {
        throw new IllegalArgumentException("nth expression must be odd/even/an+b: " + arg, e);
    }
    throw e;
}

Prevention

When it happens

Trigger: doc.select("td:nth-child(2n+1 ") with stray spaces/characters, doc.select("li:nth-child(one)"), doc.select("div:nth-child(2 n + 1)") or any non-numeric, non-odd/even expression. Parsed in QueryParser.cssNthChild.

Common situations: Typos or extra whitespace inside nth parentheses; selectors generated by template code interpolating bad values; copying nth expressions from browsers that tolerate formats jsoup's regex does not.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of jhy/jsoup@9851ac5d9c (2026-09-08). Data as JSON: /api/errors/3f75951be68fa399. Report an issue: GitHub.

Appendix: source

Thrown at src/main/java/org/jsoup/select/QueryParser.java:409

            step = 2;
            offset = 1;
        } else if ("even".equals(arg)) {
            step = 2;
            offset = 0;
        } else {
            Matcher stepOffsetM, stepM;
            if ((stepOffsetM = NthStepOffset.matcher(arg)).matches()) {
                if (stepOffsetM.group(3) != null) // has digits, like 3n+2 or -3n+2
                    step = Integer.parseInt(stepOffsetM.group(1).replaceFirst("^\\+", ""));
                else // no digits, might be like n+2, or -n+2. if group(2) == "-", it’s -1;
                    step = "-".equals(stepOffsetM.group(2)) ? -1 : 1;
                offset =
                    stepOffsetM.group(4) != null ? Integer.parseInt(stepOffsetM.group(4).replaceFirst("^\\+", "")) : 0;
            } else if ((stepM = NthOffset.matcher(arg)).matches()) {
                step = 0;
                offset = Integer.parseInt(stepM.group().replaceFirst("^\\+", ""));
            } else {
                throw new Selector.SelectorParseException("Could not parse nth-index '%s': unexpected format", arg);
            }
        }

        return ofType
            ? (last ? new Evaluator.IsNthLastOfType(step, offset) : new Evaluator.IsNthOfType(step, offset))
            : (last ? new Evaluator.IsNthLastChild(step, offset) : new Evaluator.IsNthChild(step, offset));
    }

    private String consumeParens() {
        return tq.chompBalanced('(', ')');
    }

    private int consumeIndex() {
        String index = consumeParens().trim();
        Validate.isTrue(StringUtil.isNumeric(index), "Index must be numeric");
        return Integer.parseInt(index);
    }

View on GitHub (pinned to 9851ac5d9c)