karatelabs/karate · error · JsErrorException (typeError)

cannot read properties of

Error message

cannot read properties of ${null|undefined} (reading ...)

What it means

A TypeError mirroring the browser message 'cannot read properties of null/undefined', thrown when the target of a logical assignment (`||=`, `&&=`, `??=`) through a dot or bracket access is null or undefined. RequireObjectCoercible fires before any key conversion, so `obj?.x ??= v` on a missing base throws rather than silently skipping.

Solutions

  1. Guard the base object first: `if (a) a.b ??= value;`
  2. Provide defaults at the root: `const a = raw ?? {};` before nested logical assignments
  3. Use explicit optional chaining plus separate assignment: `if (a?.b == null && a) a.b = value;`
  4. Fix the data source so the intermediate object is always present

Example fix

// before
config.redis ??= { host: 'localhost' };
// after
config = config ?? {};
config.redis ??= { host: 'localhost' };
Defensive patterns

Strategy: type-guard

Validate before calling

// JS in the script, before the logical assignment
if (config == null) config = {};
if (config.redis == null) config.redis = {};

Type guard

function hasBase(obj, path) {
  return path.split('.').every(seg => (obj = obj?.[seg]) !== null && obj !== undefined) || obj !== undefined;
}
// simpler precheck:
function isObject(v) { return v !== null && typeof v === 'object'; }

Try / catch

try {
  evalScript("config.redis ??= { host: 'localhost' };");
} catch (JsErrorException e) {
  if (e.getMessage().startsWith("cannot read properties of")) {
    evalScript("config = config ?? {}; config.redis ??= { host: 'localhost' };");
  } else throw e;
}

Prevention

When it happens

Trigger: `a.b ||= value` (or `&&=`/`??=`) where `a` evaluates to null or undefined — e.g. reading a nested config path that was never set, or an API response missing the intermediate object.

Common situations: Optional-chaining used for reads but not for the assignment target (`a?.b ??= v` still throws when `a` is undefined since the chain short-circuits to undefined, not a reference); JSON fixtures missing a nested level; map lookups returning null.

Related errors


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

Appendix: source

Thrown at karate-js/src/main/java/io/karatelabs/js/PropertyAccess.java:427

                    if (context.isStopped()) yield null;
                    context.updateSlot(slot, newValue, trackingNode);
                    yield newValue;
                }
                yield logicalCompoundRefByName(node, context, operator, rhsNode, trackingNode);
            }
            case REF_DOT_EXPR, REF_BRACKET_EXPR -> {
                AccessSite site = resolveWriteSite(node, context);
                if (site == null || site == SHORT_CIRCUIT_SITE) yield Terms.UNDEFINED;
                // JS-level throw inside the target or index eval surfaces as
                // context.isStopped() (eval returns null on abrupt completion).
                // Bail before doing any further checks so the in-flight throw
                // — not our own TypeError — is what the catch sees.
                if (context.isStopped()) yield null;
                // RequireObjectCoercible fires before ToPropertyKey on the index —
                // null/undefined target must throw TypeError before any toString
                // on a non-primitive key is invoked.
                if (site.target == null || site.target == Terms.UNDEFINED) {
                    throw JsErrorException.typeError("cannot read properties of "
                            + (site.target == null ? "null" : "undefined"));
                }
                if (site.privateName != null) {
                    Object oldValue = PrivateAccess.get(site.target, site.privateName, context);
                    if (!shouldLogicalAssign(operator, oldValue)) yield oldValue;
                    Object newValue = Interpreter.eval(rhsNode, context);
                    if (context.isStopped()) yield null;
                    PrivateAccess.set(site.target, site.privateName, newValue, context);
                    yield newValue;
                }
                if (site.receiver != null) {
                    // super reference — the fused by-index/by-name workers
                    // below have no receiver seam; run the generic sequence.
                    Object oldValue = siteRead(site, context);
                    if (context.isStopped()) yield null;
                    if (!shouldLogicalAssign(operator, oldValue)) yield oldValue;
                    Object newValue = Interpreter.eval(rhsNode, context);
                    if (context.isStopped()) yield null;

View on GitHub (pinned to a22eb90246)