karatelabs/karate · error · ParserException

optional chain is not a valid assignment target

Error message

optional chain is not a valid assignment target

What it means

This is a JavaScript early error: the left-hand side of an assignment is an optional chain (e.g. `a?.b = v`). Per the ES spec, an OptionalChain is never a valid SimpleAssignmentTarget, so the Karate JS parser rejects it at parse time. Rewrite the expression so the assignment target is a plain member or identifier reference.

Solutions

  1. Remove `?.` from the left-hand side of the assignment (use `a.b = v` when you know `a` is non-nullish).
  2. Guard the assignment: `if (a) a.b = v;` or `a && (a.b = v);`.
  3. If assigning into a nullable parent, compute the target first: `const t = a?.b;` then assign via a safe reference.

Example fix

// before
obj?.prop = value;
// after
if (obj) obj.prop = value;
Defensive patterns

Strategy: validation

Validate before calling

// reject assignments whose LHS uses optional chaining
function validAssignTarget(src) { return !/\?\.[\w$]+\s*=[^=]/.test(src); }

Try / catch

try { karate.eval(expr); } catch (e) { if (String(e).includes('optional chain is not a valid assignment target')) { /* rewrite expression */ } }

Prevention

When it happens

Trigger: Parsing JS where an assignment (including `||=`, `&&=`, `??=`) has an LHS subtree containing `?.`, e.g. `a?.b = 1` or `f()?.x ??= 2`, detected in earlyErrorNodeChecks for ASSIGN_EXPR nodes.

Common situations: Typo where the author meant `a.b = 1` but typed `a?.b`; mechanically converting null-guard code to optional chaining without removing the assignment; refactoring `a && (a.b = 1)` into `a?.b = 1`.

Related errors


AI-assisted analysis of karatelabs/karate@a22eb90246 (2026-09-12). Data as JSON: /api/errors/5d328ad6a80a0ed6. Report an issue: GitHub.

Appendix: source

Thrown at karate-js/src/main/java/io/karatelabs/parser/JsParser.java:490

     *   <li>IsValidSimpleAssignmentTarget: the LHS of {@code =} / compound assignment
     *       and the operand of {@code ++}/{@code --} must be a simple reference
     *       (identifier, member access) or a destructuring pattern. Patterns like
     *       {@code (a + b) = 1}, {@code () => {} = 1}, {@code 1 = 1}, {@code ++f()},
     *       {@code (x = y) = 1} are SyntaxErrors at parse phase.</li>
     *   <li>Optional-chain restrictions: an OptionalExpression cannot be an
     *       assignment target, the operand of {@code ++}/{@code --}, or the head of
     *       a tagged template literal.</li>
     * </ul>
     * Throws {@link ParserException} so the test262 runner classifies the failure as
     * {@code phase: parse}.
     */
    private void earlyErrorNodeChecks(Node node) {
        switch (node.type) {
            case ASSIGN_EXPR -> {
                if (node.size() > 0) {
                    Node lhs = node.getFirst();
                    if (sawOptionalChain && subtreeContainsOptionalChain(lhs)) {
                        throw new ParserException("optional chain is not a valid assignment target");
                    }
                    // Logical-assignment operators (||=, &&=, ??=) are ES2021 and have no
                    // Annex B web-compat carve-out for CallExpression LHS — the SimpleAssignmentTarget
                    // requirement is strict. Plain `=` and the older compound operators still allow
                    // `f() = …` in non-strict mode.
                    TokenType op = node.size() > 1 && node.get(1).isToken() ? node.get(1).token.type : null;
                    boolean noCallCarveOut = op == PIPE_PIPE_EQ || op == AMP_AMP_EQ || op == QUES_QUES_EQ;
                    checkSimpleAssignmentTarget(lhs, "assignment target", noCallCarveOut);
                }
            }
            case MATH_EXP_EXPR -> {
                // §13.6: the base of `**` must be an UpdateExpression — an
                // unparenthesized unary (-x, !x, ~x, +x, void x, typeof x,
                // delete x.y, await x) is a SyntaxError; ++x / --x are legal.
                Node base = stripExprWrappers(node.size() > 0 ? node.getFirst() : null);
                if (base != null) {
                    boolean unaryBase = switch (base.type) {
                        case DELETE_EXPR, TYPEOF_EXPR, UNARY_EXPR, AWAIT_EXPR -> true;

View on GitHub (pinned to a22eb90246)