Part of Learn Lua with NeoVim

Lua in Neovim: Break and Nested Loops — Star Patterns & Finding Divisible Numbers | Episode 11

Sandy LaneSandy Lane

Video: Lua in Neovim: Break and Nested Loops — Star Patterns & Finding Divisible Numbers | Episode 11 by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

Lua Break and Nested Loops: Star Patterns and Early Exits

Loops inside loops for grids and triangles. break to exit early when you've found what you're looking for. Build star rectangles, triangles, and a divisible-number search.

Today we put two loops side by side and exit early when we hit a target. The two new ideas: nesting (a loop inside a loop) and break (escape from a loop).

A star rectangle

for row = 1, 4 do
  local line = ""
  for col = 1, 8 do
    line = line .. "* "
  end
  print(line)
end

Output:

* * * * * * * * 
* * * * * * * * 
* * * * * * * * 
* * * * * * * * 

Two nested for loops. Each iteration of the outer loop:

  1. Initialise line to empty string.
  2. Inner loop appends "* " eight times.
  3. Print the row.

Four iterations of the outer loop = four rows. Eight iterations of the inner loop per row = eight stars.

A star triangle

for row = 1, 5 do
  local line = ""
  for col = 1, row do
    line = line .. "* "
  end
  print(line)
end

Output:

* 
* * 
* * * 
* * * * 
* * * * * 

The trick: the inner loop's bound is row, not a constant. Row 1 → 1 star, row 2 → 2 stars, and so on.

Same pattern works for a flipped triangle:

for row = 1, 5 do
  local line = ""
  for col = 1, 6 - row do
    line = line .. "* "
  end
  print(line)
end

Inner bound is 6 - row instead of row. Row 1 → 5 stars, row 5 → 1 star.

break: stop searching when you find

for i = 1, 200 do
  if i % 7 == 0 and i % 13 == 0 then
    print("Found: " .. i)
    break
  end
end

Output:

Found: 91

We're looking for the first number divisible by both 7 and 13. Without break, the loop would continue past 91 to 182, then quit — wasted work. With break, we stop the moment we have an answer.

break exits the innermost enclosing loop, full stop. There's no "labeled break" in Lua — to exit several levels at once, use a flag:

local found = nil
for i = 1, 100 do
  for j = 1, 100 do
    if i * j == 42 then
      found = {i, j}
      break  -- exits the inner loop only
    end
  end
  if found then break end  -- now exits the outer loop
end

Nested loops and time complexity

A nested loop where each runs N times performs N × N (= N²) operations:

for i = 1, 100 do
  for j = 1, 100 do
    -- 100 × 100 = 10,000 iterations total
  end
end

For small N this is fine. For large N (say, 10,000), nested loops mean 100 million iterations — start watching the clock.

When you need to do something to every pair of items, nested loops are unavoidable. When you can avoid them — say, by sorting first or using a hash table — do.

A grid printer

for row = 1, 3 do
  for col = 1, 3 do
    io.write("[" .. row .. "," .. col .. "] ")
  end
  io.write("\n")
end

Output:

[1,1] [1,2] [1,3] 
[2,1] [2,2] [2,3] 
[3,1] [3,2] [3,3] 

io.write is like print but doesn't add separators or newlines. Useful when building a single line piecewise.

Common nested-loop patterns

Multiplication table:

for i = 1, 10 do
  for j = 1, 10 do
    io.write(string.format("%4d ", i * j))
  end
  io.write("\n")
end

%4d pads each number to 4 characters wide for clean column alignment.

Search a 2D matrix:

local matrix = { {1,2,3}, {4,5,6}, {7,8,9} }
for r = 1, #matrix do
  for c = 1, #matrix[r] do
    if matrix[r][c] == 5 then
      print("Found at " .. r .. "," .. c)
      break  -- exits inner; outer continues
    end
  end
end

Tables get covered properly in episode 17. For now: matrix[r][c] reads the cell at row r, column c.

Pair-wise comparisons (no duplicates):

local items = {1, 2, 3, 4}
for i = 1, #items - 1 do
  for j = i + 1, #items do
    print(items[i] .. " <-> " .. items[j])
  end
end

j = i + 1 skips already-seen pairs — (1,2) is printed but (2,1) isn't.

When to use break vs restructuring

Sometimes break makes code clearer; sometimes a different loop shape is better.

break is fine when: - You're searching and want to stop on first match. - The loop has multiple exit points.

Restructure when: - The condition is "loop while X" — use while X do. - You're using flags to fake break — sometimes a function with return is cleaner.

function find_first_divisor(n)
  for i = 2, n do
    if n % i == 0 then return i end
  end
  return nil
end

return works as "break out of all the loops and the function" in one go. Often the cleanest pattern.

Common stumbles

Forgetting to reset the inner accumulator. local line = "" must be inside the outer loop, otherwise rows accumulate and you end up printing multi-line junk.

Using break to exit nested loops. It only exits the innermost. Use a flag or wrap in a function with return.

Off-by-one in the inner range. for col = 1, row and for col = 1, row - 1 look almost identical but produce subtly different patterns. Trace by hand for a small case.

Making N×N loops when N×log(N) would do. For sorting and lookups, hash tables (covered in episode 19) avoid the quadratic cost.

break outside a loop. break only exits loops — using it at the top level is a syntax error.

What's next

Episode 12: functions. Define and call your own. Parameters, return values, and a couple of utility functions.

Recap

Nested for loops for grids, triangles, multiplication tables. break exits the innermost loop. To exit multiple levels, use a flag or wrap in a function with return. Inner loop's bound can depend on the outer loop's variable for triangle/diagonal patterns. Keep an eye on the N² cost when scaling up.

Next episode: functions.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.