JavaScript Optional Chaining vs Ruby Safe Navigation

Short circuiting, or not

Posted by Owen Stephens on December 2, 2022

JavaScript (since ES2020) and TypeScript (since TypeScript 3.7) both support "optional chaining", whereby accessing a property on null or undefined does not error:  // => Cannot read properties of undefined (reading 'foo')

undefined?.foo // => undefined

This is a very similar operator to the Ruby "safe navigation" operator (added in 2.3.0) whereby calling a method on a nil receiver does not error:  # => NoMethodError: undefined method `foo' for nil:NilClass

nil&.foo # => nil

Crucial difference: short-circuiting

However, there is a crucial difference (as mentioned in the ECMAScript proposal): the JavaScript operator short-circuits whereas Ruby's doesn't. This means that in Ruby we need to "propagate" the &. to all property accesses after the first (when there is more than one), whereas in JavaScript we don't. To demonstrate, consider accessing the name of a user in the current (optional) session. First, in JavaScript:

let current_session = null;
current_session? // => undefined

current_session = { user: { name: 'Owen' } };
current_session? // => 'Owen'

Note that if current_session is null or undefined, then the evaluation short-circuits and undefined is returned immediately; if current_session is present then we expect it to have a nested property.

In Ruby the safe navigation operator does not short-circuit. We must therefore allow the result of fetching the user to be nil by using another safe navigation operator[1]:

current_session = nil

# The next line results in:
# NoMethodError: undefined method `name' for nil:NilClass
current_session& # => NoMethodError: ...
# ... so we must "propagate" the &. like so:
current_session&.user&.name # => nil

current_session ="Owen"))
current_session&  # => 'Owen'
current_session&.user&.name # => 'Owen'

As there is no short-circuiting, when current_session = nil, Ruby evaluates, which errors. With another &. Ruby first checks for nil before calling name.

JavaScript oddities

A possibly surprising fact is that, in JavaScript, adding parentheses can change the semantics of optional chaining. Specifically, when current_session = undefined then current_session? != (current_session?.user).name:

current_session? // => undefined

// The next line results in:
(current_session?.user).name // Uncaught TypeError ...
// => Uncaught TypeError: Cannot read properties of undefined (reading 'name')

However, the well-known linter ESlint has a rule to catch exactly this surprising behaviour, so in practice this subtlety should be caught before code is deployed to production.

Also note that optional and normal chaining can[2] be intermixed.

[1]: We assume two simple classes exposing user and name methods, possibly using Struct as Session = and User =

[2]: But probably shouldn't be (unless absolutely necessary) to improve readability.