flowable/flowable-engine · error · ParseException

Illegal character after ?:

Error message

Illegal character after ?: 

What it means

Thrown when '?' is followed by a character that is not a space or tab: the parser requires '?' to be the last token of its field, terminated by whitespace or end-of-string. The check tests that the next character is ' ' and the one after is '\t' (an odd compound condition), and rejects anything else. '?' is only meaningful as a placeholder meaning 'no specific value' in Day-of-Month or Day-of-Week.

Source

Thrown at modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/calendar/CronExpression.java:615

                        lastdayOfWeek = true;
                        i++;
                    }
                }

            } else {
                throw new ParseException("Illegal characters for this position: '" + sub + "'", i);
            }
            if (eval != -1) {
                incr = 1;
            }
            addToSet(sval, eval, incr, type);
            return (i + 3);
        }

        if (c == '?') {
            i++;
            if ((i + 1) < s.length() && (s.charAt(i) != ' ' && s.charAt(i + 1) != '\t')) {
                throw new ParseException("Illegal character after '?': " + s.charAt(i), i);
            }
            if (type != DAY_OF_WEEK && type != DAY_OF_MONTH) {
                throw new ParseException("'?' can only be specified for Day-of-Month or Day-of-Week.", i);
            }
            if (type == DAY_OF_WEEK && !lastdayOfMonth) {
                int val = daysOfMonth.last();
                if (val == NO_SPEC_INT) {
                    throw new ParseException("'?' can only be specified for Day-of-Month -OR- Day-of-Week.", i);
                }
            }

            addToSet(NO_SPEC_INT, -1, 0, type);
            return i;
        }

        if (c == '*' || c == '/') {
            if (c == '*' && (i + 1) >= s.length()) {
                addToSet(ALL_SPEC_INT, -1, incr, type);

View on GitHub (pinned to d6d39ce1c6)

Solutions

  1. Ensure '?' is followed by a space or is at the end of the string: '0 0 12 ? * MON' not '0 0 12 ?* MON'.
  2. When building expressions dynamically, always join fields with explicit ' ' separators.
  3. Replace '?' with '*' if you intended 'every value' rather than 'no specific value'.
  4. Validate with CronExpression.validateExpression before saving user-supplied schedules.

Example fix

// before
new CronExpression("0 0 12 ?* MON");
// after
new CronExpression("0 0 12 ? * MON");
Defensive patterns

Strategy: validation

Validate before calling

if (expr.contains("?") && java.util.regex.Pattern.compile("\\?[^\\s]").matcher(expr).find()) {
    throw new IllegalArgumentException("'?' must be followed by whitespace or end of expression: " + expr);
}

Try / catch

try {
    new CronExpression(expr);
} catch (java.text.ParseException e) {
    if (e.getMessage().contains("Illegal character after '?'")) {
        log.error("Missing space after '?' in cron: {}", expr);
    }
}

Prevention

When it happens

Trigger: new CronExpression("0 0 12 ?* * ...") — '?' immediately followed by more characters ('?*', '?L', '?15'); expression strings with missing spaces after '?'.

Common situations: Missing whitespace when concatenating fields programmatically (e.g. "... ? " + dow with dow empty or glued); template strings where a separator was dropped; hand-editing an expression and deleting the space after '?'.

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 flowable/flowable-engine@d6d39ce1c6 (2026-09-11). Data as JSON: /api/errors/1aba1eb5d3675511. Report an issue: GitHub.