karatelabs/karate · error · JsErrorException (typeError)
cannot write to optional call expression
Error message
cannot write to optional call expression
What it means
When resolving the write target of an assignment, `PropertyAccess.resolveWriteSite` encountered an optional-call expression (`f?.()` on the LHS). Optional call expressions produce no reference to assign to, so the engine raises a TypeError instead of writing to nothing.
Solutions
- Remove the optional-call from the assignment LHS and assign to a plain reference or property.
- Check for a typo: `=` used where `==`/`===` was intended.
- If conditional invocation is intended, perform the call first and assign separately.
Example fix
// before obj?.callback() = value; // TypeError: cannot write to optional call expression // after if (obj) obj.callback = value; // or: obj?.callback(value)
Defensive patterns
Strategy: validation
Validate before calling
// reject optional-call on the left of '=' before evaluating
const badLhs = /\?\.\s*\([^)]*\)\s*=|\?\.[A-Za-z_$]+\s*\([^)]*\)\s*=/.test(src);
if (badLhs) throw new Error('optional call cannot be an assignment target'); Type guard
function isPlainAssignTarget(expr) { return /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*|\[[^\]]+\])*$/.test(expr.trim()); } Try / catch
try { engine.set(varName, value); } catch (e) { if (String(e.message).includes('cannot write to optional call expression')) { /* rewrite the expression */ } else { throw e; } } Prevention
- Assign only to identifiers or property references, never `?.()` results.
- Check `=` vs `==` typos in embedded JS.
- Run the expression through a linter before embedding in features.
When it happens
Trigger: An assignment whose left-hand side contains `?.()` or an optional-chain write target, e.g. `obj?.method() = value` or `a?.[b]?.() = x` parsed as a write site.
Common situations: Stray `=` in an expression where `==` was meant; generator/template code emitting optional-call LHS; hand-edited code leaving `?.()` in an assignment target.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- cannot set on
- AggregateError requires an iterable of errors
- Array.from requires an iterable or array-like object, not
- Array.prototype.* called on null or undefined
- assignment to constant
AI-assisted analysis of karatelabs/karate@a22eb90246 (2026-09-12).
Data as JSON: /api/errors/ade9a83a5db01401.
Report an issue: GitHub.
Appendix: source
Thrown at karate-js/src/main/java/io/karatelabs/js/PropertyAccess.java:180
Node key = node.get(2);
Object target = Interpreter.eval(node.getFirst(), context);
if (target == SHORT_CIRCUITED) return SHORT_CIRCUIT_SITE;
if (context.isStopped()) return null;
if (key.token.type == TokenType.PRIVATE_NAME) {
return new AccessSite(target, null, false,
PrivateAccess.resolve(key.getText(), context));
}
return new AccessSite(target, key.getText(), false, null, superReceiver);
}
if (second.type == NodeType.REF_BRACKET_EXPR) {
Object target = Interpreter.eval(node.getFirst(), context);
if (target == SHORT_CIRCUITED) return SHORT_CIRCUIT_SITE;
if (context.isStopped()) return null;
Object index = Interpreter.eval(second.get(2), context);
if (context.isStopped()) return null;
return new AccessSite(target, index, true, null, superReceiver);
}
throw JsErrorException.typeError("cannot write to optional call expression");
}
// REF_BRACKET_EXPR
Object target = Interpreter.eval(node.getFirst(), context);
if (target == SHORT_CIRCUITED) return SHORT_CIRCUIT_SITE;
if (context.isStopped()) return null;
Object index = Interpreter.eval(node.get(2), context);
if (context.isStopped()) return null;
return new AccessSite(target, index, true, null, superReceiver);
}
/** Site read honoring a super receiver; the non-super shape delegates to
* the ordinary index/name workers unchanged. */
private static Object siteRead(AccessSite site, CoreContext context) {
if (site.receiver == null) {
return site.isIndex
? getByIndex(site.target, site.key, false, context, false)
: getByName(site.target, (String) site.key, false, context, false);
}View on GitHub (pinned to a22eb90246)