lus

Acquis 2 - Optional Chaining

Contents (11)
  1. Syntax
  2. Field access
  3. Index access
  4. Method calls
  5. Function calls
  6. False preservation
  7. Chaining behavior
  8. Motivation
    1. Verbose nil checking
    2. Conditional expressions
    3. Method safety

The ? suffix operator enables safe navigation through potentially nil values. If the value before ? is falsy (nil or false), subsequent suffix operations short-circuit and return the falsy value instead of raising an error.

Syntax#

The ? operator is placed after any expression and before a suffix operation:

expr?.field    -- field access
expr?[key]     -- index access
expr?:method() -- method call
expr?()        -- function call

When the expression is truthy, operations proceed normally. When the expression is falsy, the entire chain returns the falsy value without evaluating further.

local t = { a = { b = 42 } }
assert(t?.a?.b == 42)           -- normal access

local n = nil
assert(n?.a?.b?.c == nil)       -- short-circuits at nil

Field access#

The most common use is safely accessing fields that may not exist:

local config = { database = { host = "localhost" } }

-- Without optional chaining: verbose nil checks
local host = config and config.database and config.database.host

-- With optional chaining: concise
local host = config?.database?.host

Missing fields return nil without error:

local t = { a = {} }
local x = t?.a?.missing?.deep?.field
assert(x == nil)  -- no error

Index access#

The ? operator works with bracket notation for dynamic keys:

local data = { items = { { name = "first" }, { name = "second" } } }
assert(data?.items?[2]?.name == "second")

local key = "items"
assert(data?[key]?[1]?.name == "first")

When the base is nil, indexing short-circuits:

local t = nil
assert(t?[1] == nil)    -- no error
assert(t?["key"] == nil)

Method calls#

Methods can be safely called on potentially nil objects:

local obj = {
    value = 42,
    getValue = function(self) return self.value end
}
assert(obj?:getValue() == 42)

local missing = nil
assert(missing?:getValue() == nil)  -- no error

Function calls#

When a value might be a function or nil, ?() provides safe invocation:

local callbacks = { onSuccess = function() return "done" end }

assert(callbacks.onSuccess?() == "done")
assert(callbacks.onFailure?() == nil)  -- nil, not an error

False preservation#

Unlike nil, false short-circuits but preserves its value:

local flag = false
assert(flag?.field == false)  -- returns false, not nil

Chaining behavior#

Multiple ? operators can be chained. The first falsy value terminates the chain:

local t = { a = { b = nil } }
assert(t?.a?.b?.c?.d == nil)  -- stops at b

local t2 = nil
assert(t2?.a?.b?.c?.d?.e?.f == nil)  -- stops immediately

The ? operator only affects suffix operations that follow it. Regular operations still apply:

local t = nil
local x = t?.value or "default"
assert(x == "default")  -- nil or "default" = "default"

Motivation#

Lua does not have a built-in optional chaining operator. Accessing nested values safely requires explicit nil checks at each level.

Verbose nil checking#

Consider accessing a deeply nested configuration value:

-- Lua: explicit nil checks
local value
if config ~= nil then
    if config.server ~= nil then
        if config.server.timeout ~= nil then
            value = config.server.timeout
        end
    end
end

This pattern is verbose and error-prone. With optional chaining:

-- Lus: concise navigation
local value = config?.server?.timeout

Conditional expressions#

The common Lua idiom using and chains is verbose:

-- Lua: and-chain pattern
local port = server and server.config and server.config.port or 8080

With optional chaining:

-- Lus: cleaner with or-default
local port = server?.config?.port or 8080

Method safety#

Calling methods on potentially nil objects requires guards:

-- Lua: guarded method call
if obj ~= nil then
    obj:process()
end

With optional chaining:

-- Lus: inline safety
obj?:process()