Control Flow in Lua: if, for, while, and repeat
Control flow statements are the backbone of any programming language. They let your program make decisions, repeat actions, and handle different conditions. In this tutorial, you’ll learn every control flow mechanism Lua offers.
This is the third tutorial in our Lua fundamentals series. If you’re new to Lua, start with our earlier tutorials on installing Lua and working with variables.
The if Statement
The if statement is the most fundamental control structure. It executes a block of code only when a condition evaluates to true.
local temperature = 25
if temperature > 30 then
print("It's hot outside!")
end
The condition after if must evaluate to a boolean value. In Lua, only nil and false are falsy — everything else is truthy, including 0 and empty strings. This is different from Python or JavaScript where 0 and empty strings are falsy, so watch out when porting code or checking for uninitialized variables.
if-elseif-else Chains
For more complex decision-making, use elseif and else — Lua evaluates each branch top to bottom until a condition matches:
local score = 85
if score >= 90 then
print("Grade: A")
elseif score >= 80 then
print("Grade: B")
elseif score >= 70 then
print("Grade: C")
else
print("Grade: F")
end
You can chain as many elseif blocks as needed. Lua evaluates them top-to-bottom and stops at the first true condition. The else clause is optional and is a catch-all for any value that didn’t match. If you find yourself writing more than three or four elseif branches, consider swapping to a table-driven dispatch — it’s cleaner and often faster in Lua since table lookups are cheap.
Numeric for Loops
The numeric for loop runs a specific number of times, making it ideal when you know exactly how many iterations you need:
-- Print numbers 1 to 5
for i = 1, 5 do
print(i)
end
-- With step value
for i = 10, 0, -2 do
print(i) -- Prints: 10, 8, 6, 4, 2, 0
end
The syntax is for var = start, end, [step] do. The step defaults to 1 if omitted and can be negative for counting downward. Unlike many languages, Lua’s numeric for evaluates the start, end, and step expressions only once before the loop begins, so changing the loop variable inside the body won’t affect the iteration count.
Generic for Loops
Generic for works with iterator functions — the most common being pairs() and ipairs() , to traverse tables and other iterable collections:
local fruits = {apple = 1, banana = 2, cherry = 3}
-- Iterate over key-value pairs
for key, value in pairs(fruits) do
print(key, value)
end
-- Iterate over array-like tables
local colors = {"red", "green", "blue"}
for index, color in ipairs(colors) do
print(index, color)
end
Use pairs() for arbitrary key-value iteration and ipairs() for sequential array-style iteration. The key difference is that pairs() visits every key in the table regardless of gaps, while ipairs() walks numeric indices starting at 1 and stops at the first nil. If your table has holes , like {1, nil, 3} , ipairs will only see the first element, which can lead to subtle bugs if you’re not expecting it.
While Loops
A while loop repeats as long as its condition remains true, evaluating the condition before each iteration:
local count = 1
while count <= 5 do
print(count)
count = count + 1
end
Be careful , unlike for loops, while loops can create infinite loops if the condition never becomes false. Always make sure something inside the loop body moves the condition toward termination. A common debugging trick is to add a safety counter that breaks after a high iteration count, which catches runaway loops during development without changing your production logic.
repeat…until Loops
The repeat...until loop inverts the check: it executes the body at least once, then tests the condition at the bottom:
local input
repeat
print("Enter quit to exit")
input = io.read()
until input == "quit"
print("Goodbye!")
This is useful when you need to prompt for input before validating , the body always runs once, so you know your validation logic has something to check. A subtle difference from while is that the condition logic is inverted: repeat...until condition stops when the condition is true, whereas while condition runs while it’s true. Mixing these up is a common off-by-one mistake.
Breaking and Continuing
Use break to exit a loop immediately, jumping past all remaining iterations:
for i = 1, 10 do
if i == 5 then
break
end
print(i)
end
-- Prints: 1, 2, 3, 4
Lua doesn’t have a continue statement to skip a single iteration. The workaround is to wrap the loop body in an if block that inverts the skip condition, or to use a goto statement (available since Lua 5.2) to jump to a label near the end of the loop. For simple cases, nesting is cleaner; for deeply nested loops, goto is often clearer than piling up conditionals.
Nested control flow
You can nest control structures inside each other , a for inside a for, an if inside a while, and so on:
local matrix = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
}
for i = 1, #matrix do
for j = 1, #matrix[i] do
if matrix[i][j] % 2 == 0 then
print("Even:", matrix[i][j])
end
end
end
The inner loop iterates across columns while the outer loop moves down rows, and the if picks out even numbers. When nesting loops in Lua, keep an eye on variable scope , loop variables declared with for i = ... are local to the loop body, but any variable created without local inside the loop becomes global. Always use local inside nested loops to prevent accidentally overwriting outer variables.
Practical Example
Here’s a simple number guessing game that uses a repeat...until outer loop to keep the game running, an if-elseif chain to give the player hints, and a counter to track attempts:
local target = math.random(1, 10)
local attempts = 0
local guess
print("Guess a number between 1 and 10")
repeat
attempts = attempts + 1
io.write("Your guess: ")
guess = tonumber(io.read())
if guess < target then
print("Too low!")
elseif guess > target then
print("Too high!")
end
until guess == target
print("Correct! It took you " .. attempts .. " attempts.")
This example ties together several concepts: the repeat...until loop guarantees at least one guess, the if-elseif chain directs the player toward the answer, and attempts tracks how efficiently they solved it. Notice the explicit tonumber() call , io.read() returns a string, and comparing a string to a number with < would trigger automatic coercion in Lua, but calling tonumber() makes the intent explicit and guards against non-numeric input returning nil.
Understanding Truthiness
Lua’s truthiness rules are simpler than most languages , but they’re different enough to cause subtle bugs. Only nil and false are falsy; everything else is truthy, including zero, empty strings, and even empty tables. This affects how conditions evaluate:
local value = 0
if value then
print("Truthy!") -- This prints because 0 is truthy
end
if value ~= nil then
print("Not nil!") -- This also prints
end
This behavior differs from languages like Python or JavaScript where 0, "", and [] are falsy. The practical consequence: if you check if value then to see whether a variable was assigned, you’ll get a false positive for 0 and "" , use if value ~= nil then instead when you specifically need to detect unset variables.
Common Pitfalls
Several mistakes trip up newcomers to Lua. Here are the ones that cause the most debugging time:
Forgetting the then keyword:
-- Wrong
if temperature > 30
print("Hot")
-- Correct
if temperature > 30 then
print("Hot")
end
Lua requires the then keyword to separate the condition from the body , if you come from a language like C or Go that uses braces, this is an easy habit to break. The error message Lua gives is usually “then expected near…” which points you straight to the fix.
Off-by-one errors in numeric for loops:
-- This prints 1 to 4, not 1 to 5
for i = 1, 5 - 1 do
print(i)
end
The end value in a numeric for uses 5 - 1 instead of 5, so the loop stops at 4. This crops up most often when you’re computing loop bounds from table lengths or function return values and forget that for i = 1, #t already covers every element , adding - 1 skips the last one.
Infinite while loops:
-- Bug: count never changes
local count = 1
while count <= 5 do
print(count)
-- Missing: count = count + 1
end
Choosing the right loop
Different loops suit different situations:
- Use numeric for when you know the exact iteration count
- Use generic for when iterating over tables or custom iterators
- Use while when the loop condition depends on dynamic values
- Use repeat…until when you need the body to execute at least once
Choosing correctly makes your code clearer and less error-prone.
Summary
Lua provides straightforward control flow mechanisms:
if— Execute code conditionallyelseif— Additional conditionselse— Fallback code when all conditions are falsefor— Numeric and generic iterationwhile— Loop while condition is truerepeat...until— Loop until condition is true (always runs at least once)break— Exit a loop early
These building blocks let you create complex programs. Master them, and you’ll be writing Lua with confidence.
Next steps
Move on to Functions, Closures, and Varargs — the next tutorial in this series. Functions let you package your decision-making and looping skills into reusable, composable units.
See also
- Variables and Types in Lua — The prerequisite for this tutorial, covering local versus global scope
- Functions, closures, and varargs — The next tutorial in the fundamentals series