Skip to main content

Mojo compound statements reference

A compound statement has a header and a body. The header ends with : and is followed by an indented block with the body.

The body can contain simple statements, other compound statements, or both.

if condition:       # Header
    do_something()  # Body

The body must be indented more than the header. The first body statement sets the indentation for the rest of the body:

if condition:
    do_something()
      do_more()    # Error: statement has excess indentation

If statements

An if statement executes a block conditionally:

if x > 0:
    print("positive")
elif x < 0:
    print("negative")
elif x == 0:
    print("zero")
else:
    print("you should never get here")

Conditions are evaluated in order. Add as many as needed. The first true condition runs its block, and the statement exits. The else block runs if no condition is true.

When the body is a single simple statement, you can write it on a single line, although this is not normally recommended in many house styles:

if x > 0: print("positive")

Common shortcuts from other languages won't work in Mojo:

x > 0 and print("positive")  # Error: 'None' does not implement
                              # the '__bool__' method

print("positive") if x > 0 else pass # Error: unexpected token in expression

While loops

The while loop repeats its body while a condition is true:

var count = 0
while count < 10:
    print(count)
    count += 1

Use break to exit the loop early and continue to skip to the next iteration:

while True:
    var item = get_next()
    if item is None:
        break       # Exit loop if no more items
    if not is_valid(item):
        continue    # Skip invalid items
    process(item)  # Only runs for valid items

For loops

The for statement iterates over a sequence:

for item in items:
    process(item)

for i in range(10):   # [0, 10)
    print(i)

To support iteration, a sequence must implement __iter__() and __next__(). A for loop desugars to a while loop that uses these methods.

Destructuring works directly in the loop target. This lets you unpack tuple elements as you iterate. In this example, each item in pairs is unpacked into key and value for every iteration:

for key, value in pairs:  # For example, [("a", 1), ("b", 2), ...]
    print(key, value)

Loops and else clauses

An optional else clause runs when the loop exits normally, when the condition becomes false. It does not run if the loop exits with break:

var found = False
for item in items:
    if item == target:
        found = True
        break
else:
    print("not found")  # Only runs if break was never hit

Both for and while loops support else.

Error handling

A try statement executes code that may raise errors.

try:
    result = risky()
except e:
    handle(e)

Structure

Each try statement requires at least one except or finally clause:

try:
    operation()
except e:
    handle_error(e)   # Runs if an error occurs
else:
    on_success()      # Runs only if no error occurred
finally:
    cleanup()         # Always runs

Execution proceeds in a fixed order:

  1. The try block runs first.
  2. If an error occurs, the matching except block runs.
  3. If no error occurs, the else block (if present) runs after the try block.
  4. If included, a finally block always runs last.

Error binding

Bind the error to a name with except name:

try:
    risky()
except e:
    print(e)   # e is the caught error

Without a binding, the error is caught but not accessible. There is no default error variable. This is useful when you want to respond to an error state without needing the error details:

try:
    risky()
except:
    print("something went wrong")

Typed errors

When a function declares a specific error type with raises ErrorType, the bound variable's type is inferred:

@fieldwise_init
struct NetworkError:
    var message: String
    var code: Int

def fetch() raises NetworkError -> String:
    raise NetworkError("HTCPCP", 418)

try:
    result = fetch()
except e:              # e is inferred as `NetworkError`
    print(e.message)   # Known types supports direct field access
    print(e.code)

A try block handles one error type. The compiler raises an error if code in the try block can raise more than one error type.

Context managers

A with statement manages resources using context managers. Context managers define setup (__enter__) and cleanup (__exit__) operations. The cleanup always runs when the block exits, even if an error occurs:

with open("file.txt") as f:
    content = f.read()
# File is closed here, even if an error occurred

Multiple context managers can share a single with statement:

with open("input.txt") as f_in, open("output.txt", "w") as f_out:
    f_out.write(f_in.read())

This is equivalent to nested with statements.

How context managers work

When a with block is entered, __enter__() is called on the context manager expression. The result is bound to the as target if present. When the block exits, __exit__() is called. A context manager that defines only __enter__() is valid — __exit__() is optional.

Compile-time control flow

comptime if and comptime for run at compile time. The condition or sequence must be a compile-time value or expression.

Use them to generate code based on compile-time conditions. You cannot use runtime values in comptime statements.

comptime if

comptime if selects a branch at compile time, pruning the unselected branches. Only the selected branch appears in the compiled program.

from std.sys import size_of

comptime if size_of[Int]() == 8:
    print("64-bit")
else:
    print("Probably 32-bit")

The condition must be a compile-time expression. In this example, runtime_value is not available at compile time, so the code errors during compilation:

comptime if runtime_value > 0:   # Error: 'comptime if' requires a
    pass                         # parameter expression as a condition

comptime if supports elif and else like the regular if statement.

comptime for

comptime for unrolls a loop at compile time. Each iteration is compiled as separate code. This creates a bigger binary but improves runtime performance by eliminating loop overhead and enabling further optimizations.

comptime for i in range(3):
    print(i)   # Compiled as: print(0); print(1); print(2)

Use comptime for to generate repeated code patterns or iterate over compile-time sequences.

Scopes

Each compound statement body creates a new scope. Variables declared inside a body are not visible outside it:

if condition:
    var x = 10
print(x)   # Error: x is not in scope

with statement variables bound with as are scoped to the with block:

with open("file.txt") as f:
    data = f.read()
# f is not accessible here

Nested functions create their own scope and can capture variables from enclosing functions with the capturing keyword:

def outer():
    count = 0

    def inner() capturing:
        count += 1   # Captures count from outer

    inner()
    print(count)     # 1

Was this page helpful?