Skip to main content

Types of control flow

ConstructDescription
returnExits a function and returns a value.
ifExecutes a block if the condition is true.
if/elseExecutes different blocks based on a condition.
switchPattern matching for variants, options, results, etc.
let-elseDestructure a pattern and handle the failure case inline.
option blockEvaluates an expression and wraps the result in an option type, allowing scoped handling of null values.
label/breakAllows exiting loops early.
whileRuns while a condition is true.
forIterates over elements in a collection, terminating when no elements remain.

return

A return statement immediately exits a function or async block with a result. Unlike break or continue, which jump to a labeled point within the same function, return does not target a label. Instead, it exits the current function entirely, either returning control to the caller or, in asynchronous contexts, completing a future and resuming the caller that's awaiting the result.

Consider this function that computes the product of an array of integers.

``` motoko
func product(numbers : [Int]) : Int {
var prod : Int = 1;
for (number in numbers.vals()) {
prod *= number;
};
prod; // The implicit result of the block and function
}

This function doesn't require an explicit return. It just returns the result of its body, prod.

However, prod will remain 0 once it becomes 0 so you can save some work by returning from the function early, exiting both the loop and the function with result 0.

func product(numbers : [Int]) : Int {
var prod : Int = 1;
for (number in numbers.vals()) {
prod *= number;
if (prod == 0) return 0; // an early return can save work
};
prod; // The implicit result of the block and function
}

This also works with asynchronous functions that produce futures:

func asyncProduct(numbers : [Int]) : async Int {
var prod : Int = 1;
for (number in numbers.vals()) {
prod *= number;
if (prod == 0) return 0; // an early return completes the future
};
prod; // The implicit result of the block and function
}

If the expected return type is () then you can just write return instead of return ().

switch

A switch expression matches a value against multiple cases and executes the block of code associated with the first matching case.

import Nat "mo:base/Nat";

type HttpRequestStatus = {
#ok: Nat;
#err: Nat;
};

func checkStatus(r : HttpRequestStatus) : Text {
switch (r) {
case (#ok successCode) { "Success: " # Nat.toText(successCode) };
case (#err errorCode ) { "Failure: " # Nat.toText(errorCode) };
};
};

let-else

The let-else construct allows conditional binding of the variables in a pattern, by attempting to match a value to the pattern. The else clause handles the case when the pattern is not a match. It is useful when working with Result<T,E> and optional values (?T), enabling concise error handling or early exits when the value is null.

Since the code following the let cannot execute without its matching bindings, the else clause must have type None, typically by diverting control using return, throw, break or continue.

import Nat "mo:base/Nat";

type HttpRequestStatus = {
#ok: Nat;
#err: Nat;
};

func checkStatus(r : HttpRequestStatus) : Text {
let #ok status = r else return "The request failed!";
Nat.toText(status)
};

Unlike a switch, let-else discards any additional error information from non-matching cases, making it less suitable when detailed error handling is needed. The (#err e) case is dropped entirely; e cannot be inspected or logged.

Option block

These blocks represented as do ? {...} allow safe unwrapping of optional values using the postfix operator !, which short-circuits and exits the block with null if any value is null, simplifying code that handles multiple options. The result of the inner block, if any, is returned in an option.

 // Returns the sum of optional values `n` and `m` or `null`, if either is `null`
func addOpt(n : ?Nat, m : ?Nat) : ?Nat {
do ? {
n! + m!
}
};
// let o1 = addOpt(?5, ?2); // ?7
// let o2 = addOpt(null, ?2); // null
// let o3 = addOpt(?5, null); // null
// let o4 = addOpt(null, null); // null

Instead of having to switch on the options n and m in a verbose manner the use of the postfix operator ! makes it easy to unwrap their values but exit the block with null when either is null.

label

A label assigns a name with an optional type to a block of code that executes like any other block, but its result can be produced early using a break to the label. The type on the label should indicate the type of the block and defaults to () when omitted.

If the block produces a non-() result, the break can include a value. Labels provide more control over execution, allowing clear exit points and helping to structure complex logic effectively.

When a labeled block runs, it evaluates the block to produce a result. Labels don’t change how the block executes but enable early exits from the block using a break to that label. If the type is not () those breaks must have an argument, to use as the result of the labelled expression. Just as return exits a function early with a result, break exits its label early with a result.

func labelControlFlow() : Int {
let result = label processNumbers : Int {
let numbers : [Int] = [3, -1, 0, 5, -2, 7];
var sum : Int = 0;

for (number in numbers.values()) {
sum += number;
};
sum; // The final result of the block
};
return result;
}

break within a labeled block

A break expression immediately exits a labeled block and returns a specified value. However, break must always refer to a label identifier; it cannot be used without one.

func breakControlFlow() : Int {
let result = label processNumbers: Int {
let numbers : [Int] = [3, -1, 0, 5, -2, 7];
var sum : Int = 0;

for (num in numbers.values()) {
if (num < 0) {
break processNumbers sum; // Exit early with current sum
};
sum += num;
};
sum // This is returned if no break occurs
};
return result;
}

As with return, you can omit the value argument to a break when the type of the label is (), as in this restructured code:

func breakControlFlow() : Int {
let numbers : [Int] = [3, -1, 0, 5, -2, 7];
var sum : Int = 0;
label processNumbers {
for (num in numbers.values()) {
if (num < 0) {
break processNumbers; // Break from processNumbers
};
sum += num;
};
};
sum;
}

## `while`

A `while` loop repeatedly executes a block of code as long as a specified condition evaluates to `true`.

```motoko no-repl
import Debug "mo:base/Debug";
import Nat "mo:base/Nat";

var i = 0;
while (i < 5) {
Debug.print(Nat.toText(i));
i += 1;
}

for

A for loop iterates over the elements of an iterator, executing a block of code for each element.

import Debug "mo:base/Debug";
import Nat "mo:base/Nat";

let numbers = [1, 2, 3, 4, 5];
for (num in numbers.vals()) {
Debug.print(Nat.toText(num));
}

Program flows

In Motoko, code executes sequentially, evaluating expressions and declarations in order. However, certain constructs can alter this flow, such as exiting a block early, skipping iterations in a loop, returning a value from a function, or invoking another function.

continue

A continue expression skips the remainder of the current iteration in a loop and immediately proceeds to the next iteration. Like break, continue must reference a label and only works within a labeled while, for or loop expression.

func continueControlFlow() : Int {

// Labeled block
label processNumbers : Int {
let numbers : [Int] = [3, -1, 0, 5, -2, 7];
var sum : Int = 0;

// Labeled loop
label processing for (num in numbers.vals()) {
if (num < 0) {
continue processing; // Skip negative numbers
};
sum += num;
};
sum
};
}

Function calls

A function call executes a function by passing arguments and receiving a result. In Motoko, function calls can be synchronous (executing immediately within the same canister) or asynchronous (message passing between canisters). Asynchronous calls use async/await and are essential for inter-canister communication.

import Nat "mo:base/Nat";

persistent actor {

func processNumbers(numbers : [Int]) : Int {
var sum : Int = 0;
for (num in numbers.values()) {
if (num < 0) {
return sum;
};
sum += num;
};
return sum;
};

shared func functionCallControlFlow() : async Int {
let numbers : [Int] = [3, 1, 5, -1, -2, 7];
return processNumbers(numbers); // Function call
};

}

Execution begins in functionCallControlFlow(), where the function processNumbers() is invoked, transferring control to its logic. Inside processNumbers(), the numbers are processed one by one. If a negative number is encountered, a return statement immediately halts the function and returns the current sum. Control then flows back to functionCallControlFlow(), which receives the result and returns it.

Function calls temporarily interrupt the normal sequential flow by shifting execution to a separate block of logic. Once the called function completes, control resumes at the point where the call was made, continuing with its result.

Logo