Why do these snippets of JavaScript behave differently even though they both encounter an error?

var a = {}
var b = {}

try{
  a.x.y = b.e = 1 // Uncaught TypeError: Cannot set property 'y' of undefined
} catch(err) {
  console.error(err);
}
console.log(b.e) // 1

var a = {}
var b = {}

try {
  a.x.y.z = b.e = 1 // Uncaught TypeError: Cannot read property 'y' of undefined
} catch(err) {
  console.error(err);
}

console.log(b.e) // undefined

Solution 1:

Actually, if you read the error message properly, case 1 and case 2 throw different errors.

Case a.x.y:

Cannot set property 'y' of undefined

Case a.x.y.z:

Cannot read property 'y' of undefined

I guess it's best to describe it by step-by-step execution in easy English.

Case 1

// 1. Declare variable `a`
// 2. Define variable `a` as {}
var a = {}

// 1. Declare variable `b`
// 2. Define variable `b` as {}
var b = {}

try {

  /**
   *  1. Read `a`, gets {}
   *  2. Read `a.x`, gets undefined
   *  3. Read `b`, gets {}
   *  4. Set `b.z` to 1, returns 1
   *  5. Set `a.x.y` to return value of `b.z = 1`
   *  6. Throws "Cannot **set** property 'y' of undefined"
   */
  a.x.y = b.z = 1
  
} catch(e){
  console.error(e.message)
} finally {
  console.log(b.z)
}

Case 2

// 1. Declare variable `a`
// 2. Define variable `a` as {}
var a = {}

// 1. Declare variable `b`
// 2. Define variable `b` as {}
var b = {}

try {

  /**
   *  1. Read `a`, gets {}
   *  2. Read `a.x`, gets undefined
   *  3. Read `a.x.y`, throws "Cannot **read** property 'y' of undefined".
   */
  a.x.y.z = b.z = 1
  
} catch(e){
  console.error(e.message)
} finally {
  console.log(b.z)
}

In comments, Solomon Tam found this ECMA documentation about assignment operation.

Solution 2:

The order of operations is clearer when you exploit the comma operator inside bracket notation to see which parts are executed when:

var a = {}
var b = {}

try{
 // Uncaught TypeError: Cannot set property 'y' of undefined
  a
    [console.log('x'), 'x']
    [console.log('y'), 'y']
    = (console.log('right hand side'), b.e = 1);
} catch(err) {
  console.error(err);
}
console.log(b.e) // 1

var a = {}
var b = {}

try {
  // Uncaught TypeError: Cannot read property 'y' of undefined
  a
    [console.log('x'), 'x']
    [console.log('y'), 'y']
    [console.log('z'), 'z']
    = (console.log('right hand side'), b.e = 1);
} catch(err) {
  console.error(err);
}

console.log(b.e) // undefined

Looking at the spec:

The production AssignmentExpression : LeftHandSideExpression = AssignmentExpression is evaluated as follows:

  1. Let lref be the result of evaluating LeftHandSideExpression.

  2. Let rref be the result of evaluating AssignmentExpression.

  3. Let rval be GetValue(rref).

  4. Throw a SyntaxError exception if... (irrelevant)

  5. Call PutValue(lref, rval).

PutValue is what throws the TypeError:

  1. Let O be ToObject(base).

  2. If the result of calling the [[CanPut]] internal method of O with argument P is false, then

    a. If Throw is true, then throw a TypeError exception.

Nothing can be assigned to a property of undefined - the [[CanPut]] internal method of undefined will always return false.

In other words: the interpreter parses the left-hand side, then parses the right-hand side, then throws an error if the property on the left-hand side can't be assigned to.

When you do

a.x.y = b.e = 1

The left hand side is successfully parsed up until PutValue is called; the fact that the .x property evaluates to undefined is not considered until after the right-hand side is parsed. The interpreter sees it as "Assign some value to the property "y" of undefined", and assigning to a property of undefined only throws inside PutValue.

In contrast:

a.x.y.z = b.e = 1

The interpreter never gets to the point where it tries to assign to the z property, because it first must resolve a.x.y to a value. If a.x.y resolved to a value (even to undefined), it would be OK - an error would be thrown inside PutValue like above. But accessing a.x.y throws an error, because property y cannot be accessed on undefined.

Solution 3:

Consider the following code:

var a = {};
a.x.y = console.log("evaluating right hand side"), 1;

The rough outline of steps required to execute the code is as follows ref:

  1. Evaluate the left hand side. Two things to keep in mind:
    • Evaluating an expression is not the same as getting the value of expression.
    • Evaluating a property accessor ref e.g. a.x.y returns a reference ref consisting of base value a.x (undefined) and referenced name (y).
  2. Evaluate the right hand side.
  3. Get the value of result obtained in step 2.
  4. Set the value of the reference obtained in step 1 to the value obtained in step 3 i.e. set property y of undefined to the value. This is supposed to throw a TypeError exception ref.