suideveloper
All questions
Sui Move

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

Move
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:

Move
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:

Move
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:

Move
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:

Move
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..4

Watch 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_depth is 5 in sui-protocol-config).
  • No iterators over tables. Table and dynamic fields can't be iterated onchain. If you need to walk entries, keep keys in a vector or use a structure like LinkedTable.
  • Integer underflow. Counting down with i = i - 1 aborts when i is 0. Check before subtracting.

Sources

More on sui move