Day 5 of the 21-Day Sui Challenge.
Your HabitList can grow. Now itβs time to give it a brain. Today youβll learn control flow: the art of making your code think, decide, and choose different paths. Weβre teaching your program to check things off.
Control Flow Basics
Your code just got decision-making powers. Control flow is how programs think. Checking conditions. Making choices. Taking different paths. And it all starts with one powerful word: if.
If Statements
if (condition) {
// Executes when condition is true
}
The condition must be a bool. Move doesnβt play games here. No βtruthyβ values like JavaScript. Itβs either true or false. Period.
let count = 5;
// This works
if (count > 0) {
// do something
}
// This does NOT work
// if (count) { } // Error: expected bool, got u64
Move is strict for a reason. Blockchain code that βkinda worksβ costs people real money.
If-Else
Split the timeline. One path for true, another for false.
if (condition) {
// When true
} else {
// When false
}
Example:
public fun status_message(completed: bool): vector<u8> {
if (completed) {
b"Done!"
} else {
b"Not yet"
}
}
Simple. Powerful. Every decision your code makes starts here.
If as an Expression
Hereβs where Move gets beautiful. if isnβt just a statement. Itβs an expression that returns a value.
let message = if (score >= 60) {
b"Pass"
} else {
b"Fail"
};
Both branches must return the same type. The compiler enforces this. No surprises at runtime.
Else-If Chains
When you need more than two paths, chain your decisions:
public fun grade(score: u64): vector<u8> {
if (score >= 90) {
b"A"
} else if (score >= 80) {
b"B"
} else if (score >= 70) {
b"C"
} else if (score >= 60) {
b"D"
} else {
b"F"
}
}
The first matching condition wins. Order matters.
Safe Vector Access
Ever tried grabbing the 10th cookie from a jar that only has 3? Your program will crash just as hard. Always check before you reach.
The Problem
let habits = vector[habit1, habit2];
let third = vector::borrow(&habits, 2); // Runtime error! Index out of bounds
Move aborts if you access an invalid index. Your transaction dies. Gas vanishes. Users rage quit. Not good.
The Solution: Bounds Checking
let habits = vector[habit1, habit2];
let len = vector::length(&habits);
if (index < len) {
let habit = vector::borrow(&habits, index);
// Safe to use habit
}
Two lines of defense. Get the length. Check before you leap.
Vector Length
The vector::length function tells you how many elements exist:
let empty: vector<u64> = vector[];
let numbers = vector[10, 20, 30];
assert!(vector::length(&empty) == 0);
assert!(vector::length(&numbers) == 3);
Know your bounds. Respect your limits.
Borrowing Elements
Two flavors of borrowing, two different powers:
| Function | Returns | Use Case |
|---|---|---|
vector::borrow(&vec, i) | &T | Read element |
vector::borrow_mut(&mut vec, i) | &mut T | Modify element |
// Read-only access
let habit = vector::borrow(&list.habits, 0);
let name = habit.name; // Can read
// Mutable access
let habit = vector::borrow_mut(&mut list.habits, 0);
habit.completed = true; // Can modify
Want to change something? Borrow mutably. Just looking? Read-only is enough.
Completing Habits
Now we combine control flow and vector access into something real. Time to mark habits complete.
The Challenge
Write a function that:
- Takes a
HabitListand anindex - Checks if the index is valid
- If valid, marks that habit as completed
Three steps. Defensive programming. No crashes.
Step 1: Get the Length
public fun complete_habit(list: &mut HabitList, index: u64) {
let len = vector::length(&list.habits);
// ...
}
We need &list.habits (immutable borrow) to check the length. Just reading metadata here.
Step 2: Bounds Check
public fun complete_habit(list: &mut HabitList, index: u64) {
let len = vector::length(&list.habits);
if (index < len) {
// Safe to proceed
}
}
If index >= len, we bail out silently. No drama. In production, you might abort with an error message instead. But for learning? Graceful degradation works.
Step 3: Borrow and Modify
public fun complete_habit(list: &mut HabitList, index: u64) {
let len = vector::length(&list.habits);
if (index < len) {
let habit = vector::borrow_mut(&mut list.habits, index);
habit.completed = true;
}
}
We use vector::borrow_mut to get a mutable reference. Then flip completed to true. Done.
Complete Function
/// Mark a habit as completed by index
/// Does nothing if index is out of bounds
public fun complete_habit(list: &mut HabitList, index: u64) {
let len = vector::length(&list.habits);
if (index < len) {
let habit = vector::borrow_mut(&mut list.habits, index);
habit.completed = true;
}
// Note: In production, consider aborting on invalid index
}
Three lines of logic. A lifetime of good habits (pun intended).
Complete Solution
Hereβs the full Day 5 implementation with all bells and whistles:
/// DAY 5: Control Flow & Complete Habits
///
/// Learn:
/// - If/else statements for decision making
/// - Safe vector access with bounds checking
/// - Modifying struct fields through mutable references
module challenge::day_05 {
use std::vector;
#[test_only]
use std::unit_test::assert_eq;
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// STRUCTS
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
public struct Habit has copy, drop {
name: vector<u8>,
completed: bool,
}
public struct HabitList has drop {
habits: vector<Habit>,
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// CONSTRUCTORS (from previous days)
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
public fun new_habit(name: vector<u8>): Habit {
Habit {
name,
completed: false,
}
}
public fun empty_list(): HabitList {
HabitList {
habits: vector::empty(),
}
}
public fun add_habit(list: &mut HabitList, habit: Habit) {
vector::push_back(&mut list.habits, habit);
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// DAY 5: COMPLETE HABIT
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
/// Mark a habit as completed by index
/// Does nothing if index is out of bounds
public fun complete_habit(list: &mut HabitList, index: u64) {
let len = vector::length(&list.habits);
if (index < len) {
let habit = vector::borrow_mut(&mut list.habits, index);
habit.completed = true;
}
// Note: In a real app, you might want to abort if index is invalid
// For simplicity, we just do nothing if index is out of bounds
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// HELPER FUNCTIONS
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
/// Get the number of habits in the list
public fun habit_count(list: &HabitList): u64 {
vector::length(&list.habits)
}
/// Check if a habit at index is completed
public fun is_completed(list: &HabitList, index: u64): bool {
let len = vector::length(&list.habits);
if (index < len) {
vector::borrow(&list.habits, index).completed
} else {
false
}
}
/// Get habit name at index (returns empty if out of bounds)
public fun get_habit_name(list: &HabitList, index: u64): vector<u8> {
let len = vector::length(&list.habits);
if (index < len) {
vector::borrow(&list.habits, index).name
} else {
vector::empty()
}
}
/// Count completed habits
public fun completed_count(list: &HabitList): u64 {
let mut count = 0;
let len = vector::length(&list.habits);
let mut i = 0;
while (i < len) {
if (vector::borrow(&list.habits, i).completed) {
count = count + 1;
};
i = i + 1;
};
count
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// TESTS
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
#[test]
fun test_complete_habit() {
let mut list = empty_list();
add_habit(&mut list, new_habit(b"Exercise"));
add_habit(&mut list, new_habit(b"Read"));
add_habit(&mut list, new_habit(b"Meditate"));
// Initially all incomplete
assert!(!is_completed(&list, 0));
assert!(!is_completed(&list, 1));
assert!(!is_completed(&list, 2));
// Complete the second habit
complete_habit(&mut list, 1);
// Check status
assert!(!is_completed(&list, 0));
assert!(is_completed(&list, 1)); // Now completed
assert!(!is_completed(&list, 2));
}
#[test]
fun test_complete_out_of_bounds() {
let mut list = empty_list();
add_habit(&mut list, new_habit(b"Exercise"));
// This should do nothing (not abort)
complete_habit(&mut list, 999);
// Original habit unchanged
assert!(!is_completed(&list, 0));
}
#[test]
fun test_completed_count() {
let mut list = empty_list();
add_habit(&mut list, new_habit(b"A"));
add_habit(&mut list, new_habit(b"B"));
add_habit(&mut list, new_habit(b"C"));
add_habit(&mut list, new_habit(b"D"));
assert_eq!(completed_count(&list), 0);
complete_habit(&mut list, 0);
complete_habit(&mut list, 2);
assert_eq!(completed_count(&list), 2);
}
#[test]
fun test_complete_all() {
let mut list = empty_list();
add_habit(&mut list, new_habit(b"A"));
add_habit(&mut list, new_habit(b"B"));
add_habit(&mut list, new_habit(b"C"));
// Complete all
let len = habit_count(&list);
let mut i = 0;
while (i < len) {
complete_habit(&mut list, i);
i = i + 1;
};
assert_eq!(completed_count(&list), 3);
}
#[test]
fun test_if_expression() {
let score = 75u64;
let passed = if (score >= 60) {
true
} else {
false
};
assert!(passed);
}
}
Running the Tests
Fire up your terminal:
cd day_05
sui move test
Expected output (green lights all the way):
Running Move unit tests
[ PASS ] challenge::day_05::test_complete_all
[ PASS ] challenge::day_05::test_complete_habit
[ PASS ] challenge::day_05::test_complete_out_of_bounds
[ PASS ] challenge::day_05::test_completed_count
[ PASS ] challenge::day_05::test_if_expression
Test result: OK. 5 passed; 0 failed
Five tests. Five passes. Thatβs the sound of progress.
Try It Live with Sui CLI Web
Want to see your code run without leaving the browser? Move Dev Studio has your back:
- Open cli.firstmovers.io
- Navigate to Move section (
/app/move) - Click Build then Test

All tests passing in Move Dev Studio
Note: Day 5 functions donβt have
entry, so they canβt be called from transactions yet. On-chain interaction comes in Day 7 when we create Sui objects.
Key Takeaways
1. If Statements Require Bool
No shortcuts. No βtruthyβ values. Just honest truth.
if (count > 0) { } // OK
if (count) { } // Error!2. Always Check Bounds
Before you reach into that vector, make sure thereβs something there.
let len = vector::length(&vec);
if (index < len) {
// Safe to access
}3. Borrow Mutably to Modify
Read-only? Immutable borrow. Changing stuff? Mutable borrow.
let habit = vector::borrow_mut(&mut list.habits, index);
habit.completed = true;4. If Can Return Values
Moveβs if is an expression, not just a statement. Use that power.
let result = if (condition) { a } else { b };Both branches must return the same type. The compilerβs got your back.
Whatβs Next
Tomorrow in Day 6, weβre adding more firepower to your habit list:
- Remove habits from the list
- Reset completion status
- Advanced vector operations
Day 5 taught you to complete habits. Day 6 gives you full control: add, remove, reset, manipulate. Your list becomes truly dynamic.
Commit Your Progress
Lock it in. Make it official.
cd day_05
sui move test
git add day_05/
git commit -m "Day 5: control flow and complete habit"
Day 5 complete. Youβve learned control flow and safe vector access. These patterns arenβt just for habit trackers. They power token transfers, NFT minting, DeFi protocols, everything that runs on-chain.
Youβre building the muscle memory that separates good Move developers from great ones.
See you on Day 6.
Follow: @ercandotsui | @harry_phan06
Vietnamese Builders: Level Up Your Journey
First Movers Sprint 2026 is here!
Following the success of CommandOSS Hacker House HCMC, First Movers Sprint 2026 is designed as a stepping-stone Hacker House. A space for builders in the Sui ecosystem to sharpen their skills, accelerate product development, and prepare for the next CommandOSS Hacker House.
Whether youβre a developer or a non-technical business/growth builder, youβre welcome to join!
Why join First Movers Sprint?
- Paid internship at top Sui ecosystem projects (full-time)
- Exciting prizes and rewards
- Learn directly with top mentors from the Sui ecosystem
Co-organized by @firstmoversvn, @0xCommandOSS, and ITviec.
