Appearance
Syntax and Control Flow
Written: 2026-10-14
Learning objectives
- You tell statements apart from expressions.
- You use do blocks to limit scope.
- You pick if expressions over and or chains.
- You predict numeric for behavior.
- You write closures and recursion with base cases.
- You match each control form to its job.
Prerequisites
Statements do work, expressions produce values
Scripts mix actions that change state and values you can store.
Assignment is a statement. A call can be either. A compound assignment is a statement, never an expression. You cannot embed += inside another expression. Read the compound assignment rules in the syntax tour.
Run this pure snippet in the Command Bar.
luau
--!strict
local score = 10
score += 5
print(score)txt
15You declare score with 10, add 5 with +=, then print 15. The middle line changes state and returns nothing. You cannot write print(score += 5) because += produces no value.
Run this pure snippet in the Command Bar.
luau
--!strict
local base = 4
local doubled = base * 2
print(doubled)txt
8Here base * 2 is an expression. It produces 8. Assignment stores that value. You use expressions where Luau expects a value. You use statements where Luau expects an action.
A function call with no return is a statement in practice. A call that returns values is an expression. You decide by checking whether you use the result.
Do blocks limit scope
A do block groups statements and gives them a fresh scope. Locals declared inside end at end. You use do blocks to avoid name collisions and to release a temporary name fast.
Run this pure snippet in the Command Bar.
luau
--!strict
local total = 1
do
local total = 100
print(total)
end
print(total)txt
100
1The inner total shadows the outer one. The first print sees 100. After end, the inner name disappears. The second print sees 1. You use this pattern when you need a scratch variable with a common name.
Do blocks also help around a long calculation. Helper locals live inside, and only the final result stays outside. One-time work rarely needs a helper function. The control structure guide in functions shows how blocks nest inside functions.
If statements choose paths, if expressions choose values
Use an if statement when branches run different code. Pick the if expression when you choose between two values. The expression form looks like if cond then a else b with no end. The else branch is mandatory.
Run this pure snippet in the Command Bar.
luau
--!strict
local level = 7
local label = if level >= 5 then "veteran" else "rookie"
print(label)txt
veteranYou read this as one value choice. The condition picks "veteran". No end closes it. You can chain elseif inside the expression form too. Full syntax lives in the syntax tour.
Older Lua code often writes cond and a or b to fake a ternary. Do not copy that habit. It breaks when a is false or nil. The if expression always behaves correctly. You prefer it in every new script.
Run this pure snippet in the Command Bar.
luau
--!strict
local enabled = false
local picked = if enabled then enabled else true
print(picked)
local tricky = enabled and enabled or true
print(tricky)txt
false
trueBoth lines look similar but disagree. The if expression returns false because you asked for enabled itself. The and or chain returns true because false or true skips the falsy middle value. Only nil and false are falsy in Luau. Zero and empty strings are truthy. Stick with the if expression to avoid that bug.
Use an if statement when branches do different work.
Put this Script in ServerScriptService.
luau
--!strict
local players = 3
if players <= 0 then
print("lobby empty")
elseif players < 4 then
print("waiting for more")
else
print("match starts")
endtxt
waiting for moreEach branch prints a different message and no value flows out, which signals a statement.
Numeric for subtleties
A numeric for counts with a start, an inclusive limit, and an optional step. The step defaults to 1. The loop variable stays local to the loop. Luau evaluates the limit and step once at the start.
Run this pure snippet in the Command Bar.
luau
--!strict
for i = 1, 3 do
print(i)
endtxt
1
2
3The loop prints 1, 2, then 3. It includes the limit. This is 1-based thinking, the same habit you use for array indices.
Run this pure snippet in the Command Bar.
luau
--!strict
for i = 5, 1, -1 do
print(i)
endtxt
5
4
3
2
1A negative step counts down. You must supply it. Without it, for i = 5, 1 do runs zero times because the default step 1 can never reach 1 from 5.
Changing the limit variable inside the loop does not change the trip count. Luau freezes the limit and step before the first pass. Beginners sometimes bump a counter inside and expect more passes.
Run this pure snippet in the Command Bar.
luau
--!strict
local limit = 3
for i = 1, limit do
limit = 10
print(i)
end
print(limit)txt
1
2
3
10You set limit to 10 inside, but the loop still stops at 3. The final print proves the variable did change. The loop simply used the snapshot.
Float steps accumulate rounding error. Prefer integer counters when you need exact positions. Convert inside the body with division or multiplication. You keep loops deterministic and easy to test.
For arrays most days, a generalized for over the table reads better. Reach for numeric for when you need the index itself, a countdown, a stride, or a fixed repetition count. The iteration chapter Advanced Functions shows the generalized form side by side.
Closures remember their surroundings
A closure is a function that captures locals from its enclosing scope. The function carries those variables with it after the outer function returns. Each call to the outer function creates fresh captures.
Run this pure snippet in the Command Bar.
luau
--!strict
local function makeAdder(step: number)
return function(value: number)
return value + step
end
end
local addFive = makeAdder(5)
print(addFive(10))
print(addFive(20))txt
15
25makeAdder returns an anonymous function. That inner function remembers step. The first call adds 5 to 10. The second call adds the same 5 to 20. You use closures for counters, adders, filters, and callbacks that need memory. More shapes appear in functions and in Advanced Functions.
Loop capture trips up beginners. The loop variable is reused across passes in some languages, but each Luau pass gives the closure a value that stays correct when you copy it to a local first. Copy the value when you store closures in a loop. You avoid every closure sharing the final index.
Recursion needs a base case
A recursive function calls itself on a smaller input and stops at a base case. Without one, you overflow the stack. State the base case first when you design the function.
Run this pure snippet in the Command Bar.
luau
--!strict
local function factorial(n: number): number
if n <= 1 then
return 1
end
return n * factorial(n - 1)
end
print(factorial(5))txt
120The base case is n <= 1. It returns 1 and ends the chain. Larger inputs multiply n by the result for n - 1. The call for 5 unfolds to 5 * 4 * 3 * 2 * 1. Shrink the input on every call.
Prefer a loop when the work is a simple repetition or accumulation. Loops use constant stack space. Reserve recursion for naturally nested work such as tree walks, deep copies, and deep freezes. The deep patterns in Tables in Depth use recursion for nested tables.
Keep recursion shallow in gameplay code. Deep nests can overflow on large player-built structures. Add a depth guard or convert to an explicit stack when input size is unbounded.
When each control form fits
You now have five tools. Match the tool to the shape of the problem.
Use if statements for branching actions, if expressions for picking values, numeric for for counting, strides, and countdowns, generalized for for visiting table contents, while for open-ended conditions such as retries with a cap, closures when a function needs memory, and recursion when data nests.
Put this Script in ServerScriptService.
luau
--!strict
local rolls = {4, 6, 2}
local total = 0
for _, value in rolls do
total += value
end
local verdict = if total >= 10 then "strong" else "weak"
print(total)
print(verdict)txt
12
strongGeneralized for visits each roll, += accumulates, and one if expression picks the verdict.
Common mistakes
Wrong: you treat a compound assignment as a value.
luau
--!strict
local x = 2
print(x += 3)Studio reports a syntax error at += because a statement sits where a value belongs. Fix it with two lines. Assign first, then print.
luau
--!strict
local x = 2
x += 3
print(x)txt
5Wrong: you fake a ternary with and or and lose false.
luau
--!strict
local flag = false
local out = flag and flag or true
print(out)The output is true, but you wanted the flag itself. Fix it with an if expression.
luau
--!strict
local flag = false
local out = if flag then flag else true
print(out)txt
falseWrong: you expect a loop to notice a changed limit.
luau
--!strict
for i = 1, 2 do
print(i)
endYou run this expecting extra passes after some edit inside. Luau keeps the original limit. Fix it by choosing the right bound up front or by switching to while when the bound genuinely moves.
Wrong: recursion without a base case.
luau
--!strict
local function countdown(n: number): number
return countdown(n - 1)
endThis never stops and overflows the stack. Fix it by handling the smallest input first and returning directly.
luau
--!strict
local function countdown(n: number): number
if n <= 0 then
return 0
end
return countdown(n - 1)
end
print(countdown(3))txt
0Exercises
- Pick values with if expressions. Choose
"adult"when age reaches 18, else"minor". - Count down from 10 by 2 with numeric for. Print each step.
- Build a closure counter. Each call adds 1. Prove two counters stay independent.
- Write factorial and Fibonacci recursively, each with an explicit base case. Compare recursion against a loop version and note when you prefer each.
Solution 1
Run this pure snippet in the Command Bar.
luau
--!strict
local age = 20
local group = if age >= 18 then "adult" else "minor"
print(group)txt
adultYou pick one of two strings with a single expression. No end closes it.
Solution 2
Run this pure snippet in the Command Bar.
luau
--!strict
for i = 10, 1, -2 do
print(i)
endtxt
10
8
6
4
2You count down with step negative 2. The limit 1 is inclusive, so the last printed value is 2.
Solution 3
Run this pure snippet in the Command Bar.
luau
--!strict
local function makeCounter(): () -> number
local count = 0
return function()
count += 1
return count
end
end
local a = makeCounter()
local b = makeCounter()
print(a())
print(a())
print(b())txt
1
2
1Each maker call creates a fresh count. Calls to a do not move b. That independence proves the capture is per closure.
Solution 4
Run this pure snippet in the Command Bar.
luau
--!strict
local function factorial(n: number): number
if n <= 1 then
return 1
end
return n * factorial(n - 1)
end
local function fib(n: number): number
if n <= 1 then
return n
end
return fib(n - 1) + fib(n - 2)
end
local total = 1
for i = 2, 5 do
total *= i
end
print(factorial(5))
print(fib(6))
print(total)txt
120
8
120Recursion fits naturally nested definitions. The loop fits plain accumulation because it uses constant stack space.
Summary
- Statements act. Expressions produce values. Compound assignments are statements.
- Do blocks scope temporaries. If expressions pick values. If statements branch actions.
- Numeric for snapshots its limit and step. Float steps risk drift.
- Closures carry memory. Recursion needs a base case and a smaller input.
- Next: Tables in Depth.