luaguides

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 conditionally
  • elseif — Additional conditions
  • else — Fallback code when all conditions are false
  • for — Numeric and generic iteration
  • while — Loop while condition is true
  • repeat...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