Conditionals
Lua’s principal conditional construct is if ... then ... elseif ... then ... else ... end. The condition admits any expression — only nil and false are falsy; all other values are truthy (including 0, "", {}). Lua does not admit if as expression — the conventional substitute is the and-or trick (cond and a or b). Lua admits no ternary operator. The combination — strict nil/false truthiness, the keyword-delimited blocks, the and-or substitute for ternary, the absence of unless/switch — is the substance of Lua’s selection surface.
if / elseif / else
The principal form:
if condition then
-- body
elseif other then
-- body
else
-- body
end
Examples:
if x > 0 then
print("positive")
elseif x < 0 then
print("negative")
else
print("zero")
end
The elseif is one keyword (not else if) — distinct from C-family languages. The then is required after each condition; end terminates the entire chain.
For single-line admit:
if x > 0 then print("positive") end
if x > 0 then print("positive") else print("non-positive") end
Truthiness
Only nil and false are falsy:
if 0 then print("yes") end -- prints (0 is truthy)
if "" then print("yes") end -- prints
if {} then print("yes") end -- prints
if nil then print("yes") end -- nothing
if false then print("yes") end -- nothing
The strictness eliminates the C-family truthiness pitfalls. The conventional defences:
- Use explicit comparisons —
if n ~= 0,if s ~= "". - Use
next(t)for empty table check —if next(t) ~= nil.
And-or ternary
Lua does not admit a ternary operator. The conventional substitute uses and/or:
local max = (a > b) and a or b
local sign = (n > 0) and "positive" or (n < 0) and "negative" or "zero"
The mechanism:
cond and areturnsaifcondis truthy, elsecond.(cond and a) or breturnsaifcondis truthy, elseb.
The principal pitfall: if a is false or nil, the pattern returns b (the “else” value):
local x = true and false or "default" -- "default" (false treated as falsy)
The defence is explicit if/else for cases involving false/nil:
local x
if condition then x = false else x = "default" end
Default value with or
The or admits substantial default-value patterns:
local options = options or {}
local timeout = options.timeout or 30
local name = arg or "world"
The form returns the first truthy operand. The same pitfall applies — if arg is false, the default is used:
local enabled = false
local effective = enabled or true -- true (false is falsy!)
-- Defence: explicit nil check:
local effective
if enabled == nil then
effective = true
else
effective = enabled
end
Compound conditions
if a > 0 and b > 0 then
-- both positive
end
if a > 0 or b > 0 then
-- at least one positive
end
if not is_ready then
-- not ready
end
-- With multiple checks:
if user and user.active and user:has_permission("delete") then
-- ...
end
Ranges
For range checks, no chained comparison — Lua does not admit 0 < n < 100:
-- Wrong (works but produces unexpected behaviour):
-- if 0 < n < 100 then ... end
-- Right:
if n > 0 and n < 100 then
-- ...
end
-- Or:
if 0 < n and n < 100 then
-- ...
end
if as statement only
if does not return a value:
-- Cannot:
-- local max = if a > b then a else b end -- ERROR
-- Conventional substitutes:
local max = a > b and a or b -- and-or trick
-- Or explicit:
local max
if a > b then max = a else max = b end
nil and false distinction
The two falsy values are distinct:
print(nil == false) -- false (different values)
local x -- nil (uninitialised)
local y = false -- false (explicit)
if x == nil then ... end -- explicit nil check
if y == false then ... end
For checking “neither nil nor false”:
if value then ... end -- truthy check
For checking “is nil specifically”:
if value == nil then ... end -- nil only
Common patterns
Early return
local function process(input)
if input == nil then return "null input" end
if input == "" then return "empty input" end
if #input > 100 then return "too long" end
return input:upper()
end
The pattern admits substantial linear code paths.
Validate-and-bail
local function divide(a, b)
if b == 0 then
error("division by zero")
end
return a / b
end
local function process(input)
if type(input) ~= "string" then
error("expected string, got " .. type(input))
end
-- ...
end
Default with or
local function greet(name, greeting)
name = name or "world"
greeting = greeting or "Hello"
return greeting .. ", " .. name
end
print(greet()) -- "Hello, world"
print(greet("Alice")) -- "Hello, Alice"
print(greet("Alice", "Hi")) -- "Hi, Alice"
Conditional value with and-or
local sign = (n > 0) and 1 or (n < 0) and -1 or 0
local label = active and "running" or "stopped"
local size = (#items > 10) and "large" or "small"
Validation chain
local function validate(form)
local errors = {}
if form.name == "" or form.name == nil then
table.insert(errors, "name required")
end
if form.age == nil or form.age < 0 then
table.insert(errors, "invalid age")
end
if form.email and not form.email:match("@") then
table.insert(errors, "invalid email")
end
return errors
end
Empty checks
-- Empty string:
if not s or s == "" then
print("no string")
end
-- Empty table:
if not t or next(t) == nil then
print("no table or empty")
end
-- Empty array:
if not arr or #arr == 0 then
print("no array or empty")
end
Nested conditions
local function categorize(score)
if score < 0 then return "invalid" end
if score >= 90 then return "A" end
if score >= 80 then return "B" end
if score >= 70 then return "C" end
if score >= 60 then return "D" end
return "F"
end
The early-return pattern is conventionally clearer than nested if/elseif.
Conditional method call
if callback then
callback(result)
end
-- With and-or for "call if non-nil and use result":
local result = handler and handler(data) or default
Optional chain
local name = user and user.profile and user.profile.name or "anonymous"
-- Or with an explicit helper:
local function dig(t, ...)
for _, key in ipairs({...}) do
if t == nil then return nil end
t = t[key]
end
return t
end
local name = dig(user, "profile", "name") or "anonymous"
The chaining via and admits substantial nil-safe access; the dig helper admits substantial readability.
Boolean from condition
local is_admin = user.role == "admin"
local is_valid = email:match("@") ~= nil and #password >= 8
Dispatch via if-elseif
local function handle_command(cmd, args)
if cmd == "help" then
show_help()
elseif cmd == "version" then
show_version()
elseif cmd == "run" then
run_with_args(args)
elseif cmd == "quit" then
return false -- signal exit
else
print("unknown command: " .. cmd)
end
return true
end
For substantial dispatch, a table-based approach is conventionally clearer:
local commands = {
help = function() show_help() end,
version = function() show_version() end,
run = function(args) run_with_args(args) end,
quit = function() return false end
}
local function handle(cmd, args)
local handler = commands[cmd]
if handler then
return handler(args)
else
print("unknown command: " .. cmd)
return true
end
end
Explicit nil check vs truthy check
-- Truthy check (any falsy value triggers):
if value then ... end
-- Explicit nil check (only nil triggers):
if value == nil then ... end
-- Explicit false check:
if value == false then ... end
The conventional discipline:
- Use truthy check (
if value then) for substantial general use. - Use explicit nil check (
if value == nil then) whenfalseis a valid value.
Conditional table field
local options = {host = host, port = port}
if use_ssl then
options.ssl = true
options.cert = cert_path
end
-- Or with table constructor and conditional:
local options = {
host = host,
port = port,
ssl = use_ssl and true or nil
}
Trailing condition
The repeat ... until admits a trailing condition:
repeat
local input = io.read()
process(input)
until input == "quit"
Treated in Loops.
Switch-like via table
local actions = {
[1] = function() print("one") end,
[2] = function() print("two") end,
default = function() print("other") end
}
local action = actions[n] or actions.default
action()
The pattern admits substantial dispatch via table lookup.
A note on the absence of unless / switch
Lua does not admit:
unless— useif not cond then.switch— useif/elseif/elsechain or table dispatch.when(Kotlin-style) — useif/elseif/else.
The mechanism admits fewer constructs; conventional table-dispatch and if-chains cover the substantial use cases.
A note on the conventional discipline
The contemporary Lua conditional advice:
- Use
if/elseif/else/endfor the standard branching. - Use the and-or trick for short value-returning ternaries.
- Use
orfor default values — bewarefalse/nilvalid values. - Use
andfor chained nil-safe access. - Use early returns for precondition validation.
- Use explicit nil checks (
x == nil) whenfalseis admitted. - Use table dispatch for substantial multi-way branching.
- Trust strict truthiness — only nil/false are falsy.
- Use explicit comparisons — clearer than truthy checks.
The combination — if/elseif/else/end blocks, the strict-nil/false truthiness, the and-or ternary substitute, the absence of switch, the keyword-delimited construction — is the substance of Lua’s selection surface. The discipline produces straightforward, explicit conditional code with substantial brevity through the and-or patterns.