How do loops work in Move?
November 6, 2026
Move has two loop constructs: while (cond) { ... } and the infinite loop { ... }, controlled with break and continue. There is no for keyword. To iterate over a vector or a range you normally use standard-library macros such as v.do!(|x| ...), v.fold!(...) or 10u64.do!(|i| ...), which expand into while loops at compile time. Every iteration costs gas, so a loop that runs too long aborts the transaction when the gas budget runs out.
while
fun sum_to(n: u64): u64 {
let mut i = 0;
let mut sum = 0;
while (i < n) {
i = i + 1;
sum = sum + i;
};
sum
}The condition is checked before each iteration. A while is an expression of type (), so put a semicolon after it if more code follows.
loop, break and continue
loop runs until you break. Unlike while, a loop can return a value through break:
fun first_even(v: &vector<u64>): Option<u64> {
let mut i = 0;
loop {
if (i >= v.length()) break option::none();
if (v[i] % 2 == 0) break option::some(v[i]);
i = i + 1;
}
}continue skips to the next iteration:
let mut i = 0;
let mut evens = 0;
while (i < 10) {
i = i + 1;
if (i % 2 == 1) continue;
evens = evens + 1;
};Labels for nested loops
break and continue act on the innermost loop. To exit an outer loop, label it with a quote-prefixed name:
fun grid_contains(grid: &vector<vector<u8>>, target: u8): bool {
let mut row = 0;
'search: loop {
if (row >= grid.length()) break false;
let inner = &grid[row];
let mut col = 0;
while (col < inner.length()) {
if (inner[col] == target) break 'search true;
col = col + 1;
};
row = row + 1;
}
}Plain blocks can be labeled too, and exited with return 'label value. Labels go on loop, while or a {} block, not on if. break/continue use loop labels, return uses block labels.
Iterating with macros
Most real code skips hand-written loops. The standard library defines macros on vector and on each integer type:
let v = vector[1, 2, 3, 4];
let mut total = 0;
v.do_ref!(|n| total = total + *n); // borrow each element
let doubled = v.map_ref!(|n| *n * 2);
let evens = v.count!(|n| *n % 2 == 0);
let has_three = v.any!(|n| *n == 3);
let sum = v.fold!(0, |acc, n| acc + n); // takes v by value
let mut s = 0;
10u64.do!(|i| s = s + i); // i = 0..9
1u64.range_do!(5, |i| s = s + i); // i = 1..4Watch the variants: do!, map! and fold! take the vector by value (consuming it), while do_ref!, do_mut! and map_ref! work on references. There are also filter!, find_index!, all!, zip_do!, tabulate! and others in std::vector. Integer types have do!, do_eq!, range_do! and range_do_eq! (the _eq versions include the end value).
Macros are expanded at compile time into ordinary loop code, so there is no closure or function-call overhead at runtime.
Gotchas
- Gas. An unbounded loop over user-supplied data can make a function too expensive to call. Keep collections bounded, or paginate work across transactions. Set a sensible gas budget and dry-run to measure.
- Nesting limit. The bytecode verifier caps nested loop depth (
max_loop_depthis 5 insui-protocol-config). - No iterators over tables.
Tableand dynamic fields can't be iterated onchain. If you need to walk entries, keep keys in a vector or use a structure likeLinkedTable. - Integer underflow. Counting down with
i = i - 1aborts wheniis 0. Check before subtracting.
Sources
- Control Flow, The Move Book
- Vector, The Move Book
std::vectorandstd::u64source, MystenLabs/sui