Beginner

Control Flow in Lua

Learn conditionals, loops, and loop control in Lua—if/elseif/else, while, repeat-until, numeric and generic for, break, and the goto idiom—with Docker-ready examples

Control flow is how a program decides what to run and how many times to run it. As a dynamically typed scripting language, Lua keeps its control structures small and consistent: a handful of keywords cover every branch and loop you will need, and each block is closed with an explicit end (or until) rather than relying on indentation or braces.

Two things make Lua’s control flow distinctive. First, its notion of truthiness is unusually strict: only nil and false are falsy. Every other value—including 0, the empty string "", and empty tables {}—is truthy. This trips up programmers coming from C, Python, or JavaScript, where 0 and "" are falsy. Second, Lua deliberately omits a few features other languages have: there is no ternary operator and no continue statement. Instead, Lua provides idioms—the and/or short-circuit trick and the goto label—that achieve the same results.

In this tutorial you will learn conditionals (if/elseif/else), all four of Lua’s loops (while, repeat-until, numeric for, and generic for), and loop control with break and goto. Every example is self-contained and runnable in Docker.

Conditionals: if / elseif / else

Lua’s conditional uses if ... then, optional elseif ... then branches, an optional else, and a closing end. Note that it is elseif (one word), not else if.

Create a file named conditionals.lua:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
-- Branch on a value
local temperature = 22

if temperature > 30 then
    print("It's hot")
elseif temperature >= 15 then
    print("It's mild")
else
    print("It's cold")
end

-- Truthiness: only nil and false are falsy.
-- 0 and "" are TRUTHY in Lua (unlike C, Python, or JavaScript).
local value = 0
if value then
    print("0 is truthy in Lua")
end

-- A nil value is falsy, so 'not nil' is true
local name = nil
if not name then
    print("name is not set")
end

-- Logical operators are spelled out: and / or / not
local age = 20
if age >= 18 and age < 65 then
    print("Working age")
end

-- Lua has NO ternary operator. The idiom is: cond and a or b
local status = (age >= 18) and "adult" or "minor"
print("Status: " .. status)

The cond and a or b idiom works because and/or short-circuit and return one of their operands (not a boolean): and returns its second operand when the first is truthy, and or returns its second operand when the first is falsy. The one caveat: it breaks if a itself can be nil or false, in which case the or b branch fires unexpectedly.

Loops: while, repeat-until, and numeric for

Lua offers four loops. The while loop tests its condition before each iteration; repeat-until tests after, so its body always runs at least once. The numeric for counts from a start value to a stop value with an optional step.

Create a file named loops.lua:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
-- Numeric for: for var = start, stop[, step] do ... end
-- The range is inclusive of both endpoints.
print("Numeric for loop:")
for i = 1, 5 do
    io.write(i .. " ")
end
print()

-- A negative step counts downward
print("Countdown:")
for i = 3, 1, -1 do
    io.write(i .. " ")
end
print()

-- While loop: condition checked before each pass
print("While loop (powers of two):")
local n = 1
while n <= 16 do
    io.write(n .. " ")
    n = n * 2
end
print()

-- Repeat-until: body runs first, condition checked at the end.
-- Note the condition is the EXIT condition (loop stops when true).
print("Repeat-until:")
local count = 0
repeat
    count = count + 1
    io.write(count .. " ")
until count >= 3
print()

-- Generic for with ipairs: ordered iteration over an array-style table
print("ipairs over an array:")
local fruits = {"apple", "banana", "cherry"}
for index, fruit in ipairs(fruits) do
    print(index .. ": " .. fruit)
end

Here io.write prints without a trailing newline (unlike print), so the bare print() calls add the line breaks. Remember Lua arrays are 1-indexed: ipairs starts counting at 1, not 0.

Loop control: break and the goto idiom

break exits the innermost loop immediately. Lua has no continue keyword; the idiomatic replacement is a goto jump to a ::continue:: label placed at the end of the loop body.

Create a file named loop_control.lua:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
-- break stops the loop as soon as a condition is met
print("Finding first multiple of 7:")
for i = 1, 100 do
    if i % 7 == 0 then
        print("Found: " .. i)
        break
    end
end

-- Lua has no 'continue'. Use goto with a label to skip to the next pass.
print("Odd numbers from 1 to 10:")
for i = 1, 10 do
    if i % 2 == 0 then
        goto continue
    end
    io.write(i .. " ")
    ::continue::
end
print()

-- break only exits the INNERMOST loop
print("Products, skipping any that exceed 6:")
for a = 1, 3 do
    for b = 1, 3 do
        if a * b > 6 then
            break
        end
        io.write(a .. "x" .. b .. "=" .. (a * b) .. "  ")
    end
end
print()

In the odd-numbers loop, even values jump straight to ::continue:: (the last statement in the body) and skip the io.write. In the nested loop, break only ends the inner b loop—the outer a loop keeps going.

Stable iteration with pairs

The generic for also works with pairs, which iterates over all keys of a table, including string keys. Important: pairs does not guarantee any particular order. When you need a predictable order, collect the keys into an array and sort them.

Create a file named iterate_table.lua:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
local scores = {math = 90, science = 85, history = 78}

-- pairs() visits every key, but the order is unspecified.
-- Collect the keys, then sort them for deterministic output.
local subjects = {}
for subject in pairs(scores) do
    subjects[#subjects + 1] = subject
end
table.sort(subjects)

for _, subject in ipairs(subjects) do
    print(subject .. " = " .. scores[subject])
end

The #subjects + 1 expression uses the length operator # to append to the end of the array. The underscore _ is a conventional name for a loop variable you do not intend to use (here, the numeric index from ipairs).

Running with Docker

1
2
3
4
5
6
7
8
# Pull the official image
docker pull nickblah/lua:5.4-alpine

# Run each example
docker run --rm -v $(pwd):/app -w /app nickblah/lua:5.4-alpine lua conditionals.lua
docker run --rm -v $(pwd):/app -w /app nickblah/lua:5.4-alpine lua loops.lua
docker run --rm -v $(pwd):/app -w /app nickblah/lua:5.4-alpine lua loop_control.lua
docker run --rm -v $(pwd):/app -w /app nickblah/lua:5.4-alpine lua iterate_table.lua

Expected Output

Running conditionals.lua:

It's mild
0 is truthy in Lua
name is not set
Working age
Status: adult

Running loops.lua:

Numeric for loop:
1 2 3 4 5 
Countdown:
3 2 1 
While loop (powers of two):
1 2 4 8 16 
Repeat-until:
1 2 3 
ipairs over an array:
1: apple
2: banana
3: cherry

Running loop_control.lua:

Finding first multiple of 7:
Found: 7
Odd numbers from 1 to 10:
1 3 5 7 9 
Products, skipping any that exceed 6:
1x1=1  1x2=2  1x3=3  2x1=2  2x2=4  2x3=6  3x1=3  3x2=6  

Running iterate_table.lua:

history = 78
math = 90
science = 85

Key Concepts

  • Only nil and false are falsy. Everything else—0, "", {}—is truthy. This is one of Lua’s biggest gotchas for newcomers.
  • Blocks close with end. if, while, for, and function bodies all end with end; the repeat loop closes with until <condition> instead.
  • It’s elseif, one word. Writing else if opens a nested if that needs its own end.
  • No ternary operator. Use the cond and a or b short-circuit idiom—but beware when a can be nil or false.
  • No continue keyword. Jump to a ::continue:: label with goto to skip to the next iteration.
  • Four loops, distinct uses. while tests before, repeat-until tests after (runs at least once), numeric for counts a range, generic for walks a table.
  • repeat-until checks an exit condition. The loop stops when the condition becomes true—the opposite sense of while.
  • ipairs is ordered, pairs is not. Use ipairs for arrays (stopping at the first nil); use pairs for all keys, sorting first if you need a stable order. Remember arrays are 1-indexed.

Running Today

All examples can be run using Docker:

docker pull nickblah/lua:5.4-alpine
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining