Lua Gotchas, Footguns and Other Hazards¶
While not part of the style guide proper, this page collects common but not obvious issues within Lua code. Such issues tend to catch people out leading to hard to track down errors. The first defense against such errors is knowing they can exist.
See also: https://www.luafaq.org/gotchas.html
Boolean Operations on Non-Boolean Values¶
A common pattern in our codebase is to use and
and or
to imitate a ternary operator. This works because anything other than nil
or false
evaluates as truthy, and the boolean operators use short-circuit logic and return the value of the last operand evaluated. So:
local a = {u="v"} local b = 1 local c = false local d = a or b -- d equals a local e = c and a or b -- e equals b
The problem is this gives an unexpected answer if the middle term is falsy.
local a = getX() local b = getY() local c = true local d = c and a or b
With a ternary operator, you would always expect d
to equal a
, but if getX()
returns nil
, then the and
becomes true and nil
, which evaluates to nil
, and then nil or b
evaluates to b
.
if-then-else expressions¶
We now have if
expressions that can replace this pattern with a safer and more readable alternative. It's even faster to boot.
local a = getX() local b = getY() local c = true local d = if c then a else b -- No problem if a is falsy.
Arrays¶
Arrays in Lua are effectively tables with numerical indices, and the length operator #
only counts the contiguous numerical indices starting with 1. This applies to the ipairs
iterator function as well.
local a = { 0 = "not counted", 1 = "counted", 2 = "counted", 3 = "counted", 5 = "not counted", X = "not counted", } local n = #a -- 3 for k, v in ipairs(a) do print(k, v) -- Prints the three "counted" values end
Length and Sparse Arrays¶
Don't count on the length operator to correctly count the contiguous portion of a sparse array. Internally, it does a binary search to calculate the length, and sometimes sparse arrays can give unexpected results.
If you need to know the length of a sparse array, you will need to keep track of it yourself. Use the same pattern that table.pack
does and store the length in yourSparseArray.n
.
Nil Values¶
Because setting a table value to nil removes that key, nil values in arrays can throw off the expected length.
local a = {1, 2, 3, 4, 5} a[4] = getSomething() -- happens to return nil local n = #a -- 3
1-Based Index Math¶
Remember that Lua indices start at 1 if you are doing math on indices. For example, the first index in a list will not be zero mod 2, and when wrapping a large number into an index, you need to add 1 after modding.
Return Values¶
Returning Nothing¶
There is a difference between returning nil
and returning nothing, but it is not always obvious.
local function zero() return end local function one() return nil end local a = {1} table.insert(a, one()) -- Inserting nil leaves a unchanged table.insert(a, zero()) -- Error! wrong number of arguments to 'insert'
Make sure if your function ever returns any value, even nil
, every return in your function returns some value, at least nil
.
Returning multiple values¶
Lua functions can return multiple values, but the extra values can be lost in some slightly unexpected situations. In particular, only the last function in a list will be expanded. All others will only retain their first output.
local function values(n) if n == 1 then return 1 elseif n == 2 then return 1, 2 else return 1, 2, 3 end end print(values(2)) -- 1 2 print(values(1), values(2)) -- 1 1 2 print(values(3), values(2)) -- 1 1 2 (!) print(values(3), values(2), 1) -- 1 1 1 (!)
A more realistic example of where this might come up is in a function that uses default values:
local function position(x, y, z) z = z or 0 -- default z to 0 -- ... end position(getX(), getY())
If getY
returns a second value some of the time, this can unintentionally change z
.
Forcing a Single Return¶
Both of the above problems are frequently hidden because there are several situations where Lua will force the results into one value.
- Putting parentheses around an expression will force exactly one value, either the first if there are multiple, or
nil
if there were zero. - Assigning to a list of variables will force each variable to take a value, filling in the extras with
nil
. - As mentioned above, only the last set of values in a sequence of arguments will be expanded. Anything before that will be reduced to a single value.
Roblox Types as Table Keys¶
Instances are the only Roblox type that are safe to use as a table key. Even then, instances should never be used as keys in a weak table as their collection semantics are unintuitive and not useful.