Control Flow
Branching
Two constructs cover all decisions. Use if when the condition is a boolean expression, and case when you are selecting between known alternatives — especially enumerations, where the compiler can prove you handled every value.
if / elif / else
let temperature = 21
if temperature > 30:
echo "hot"
elif temperature > 15: # 'elif' chains without extra nesting
echo "mild" # this branch runs: 21 > 15
else:
echo "cold"
# Conditions are plain booleans; there is no truthiness for integers.
let enabled = true
if enabled and temperature < 25:
echo "comfortable"
case, Ranges and Sets
case is exhaustive for enumerations and supports ranges and sets of values, which turns a chain of comparisons into a table of openings.
type Grade = enum gA, gB, gC, gF
proc points(g: Grade): int =
case g # no 'else' needed: every enum value is covered
of gA: 4 # each branch is an expression returning an int
of gB: 3
of gC: 2
of gF: 0
echo points(gB) # 3
proc classify(ch: char): string =
case ch
of 'a' .. 'z': "lower" # ranges are allowed in case labels
of 'A' .. 'Z': "upper"
of '0' .. '9': "digit"
of '_', '-': "separator" # a list of alternatives
else: "other" # exhaustive checks end here, so 'else' is required
echo classify('7'), " ", classify('_')
Loops
while repeats until a condition fails; for walks a collection or an iterator. In practice for is used far more often, because it cannot run away with an index.
while Loops
var fuel = 3
while fuel > 0: # condition checked before every iteration
echo "burning, fuel=", fuel
dec fuel # decrement by one — clearer than fuel = fuel - 1
# A 'while true' loop needs an explicit exit; name the loop when you break it.
var attempts = 0
block retry:
while true:
inc attempts
if attempts >= 3:
break retry # leaves the labeled block, not just the loop
echo attempts # 3
for Loops and Iterators
The for loop drives iterators, so the same syntax walks ranges, sequences, tables and files. Use countup and countdown when you need a step other than one.
for i in 0 .. 4: # inclusive range: 0 1 2 3 4
echo i # echo ends the line; use stdout.write to stay inline
for i in countup(0, 10, 2): # explicit step: 0 2 4 6 8 10
echo i
for i in countdown(3, 1): # descending
echo i
let names = @["Ada", "Grace", ""]
for name in names: # walking a sequence yields values, not indices
if name.len == 0:
continue # skip this item, keep iterating
echo "name: ", name
# When the index matters, iterate with an index operator over a range.
for i in 0 ..< names.len: # '..<' is the half-open operator: no off-by-one
echo i, " -> ", names[i]
Control Flow as Expressions
Because the branches of if, case and block are expressions, they can be assigned, returned or passed as arguments. Prefer this over declaring a mutable variable that is then filled in.
Assigning a Decision
import std/strformat
proc describe(count: int): string =
# No temporary variable: the function body IS the expression.
if count == 0: "empty"
elif count == 1: "single"
else: &"{count} items" # interpolation needs the import above
echo describe(0), "/", describe(1), "/", describe(5)
Early Exit with a Value
A named block combined with break value gives you a structured early return inside an expression, which keeps the success path first and the fallback last.
proc firstDigit(words: seq[string]): int =
block search:
for word in words:
for ch in word: # nested loop, still one exit point
if ch in '0' .. '9':
break search (ord(ch) - ord('0')) # yields a value from the block
-1 # fallback: nothing numeric was found
echo firstDigit(@["age:", "42"]) # 4
echo firstDigit(@["nope"]) # -1
# Cases that must not fall through use 'return' explicitly, which is fine too:
proc clamp(value, low, high: int): int =
if value < low: return low
if value > high: return high
value
echo clamp(150, 0, 100) # 100
Practice
Write a fizzbuzz for 1 to 20 using for and one if/elif chain, then rewrite it so the body is a single case expression over (n mod 3, n mod 5). Comparing both versions shows why expression-oriented control flow compresses code without hiding logic. Next: Procedures & Scope.