Acquis 2 - Optional Chaining
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()