Book
Statements

Statements

The following statements can appear anywhere in the function body.

let statement

The let statement allows local and block-scoped variable declaration.

In Tact, declaring a local variable always requires an initial value. However, the type ascription can be omitted and Tact will try to infer it from the initial value:

let value: Int = 123; // full declaration with type and value
let vInferred = 123;  // inferred type Int
 
let vExplicitCtx: Context = context(); // explicit type Context, a built-in Struct
let vCtx = context();                  // inferred type Context

Note, that initial value of null can mean both an empty map<K, V> with arbitrary K and V types, and the intentional absence of any other value for the optional type. That's why whenever you're declaring an optional or a map<K, V>, you'll need to explicitly specify the type as it cannot be inferred:

let vOptional: Int? = null; // explicit type Int or null
let vOptInt = 42;           // implicit type Int
vOptInt = null;             // COMPILATION ERROR!
 
let vMap: map<Int, Int> = emptyMap(); // explicit type map<Int, Int>
let vMapWithSerialization: map<Int as uint8, Int as uint8> = emptyMap();

Naming a local variable with underscore _ makes its value considered unused and discarded. This is useful when you don't need a return value of some function with side effects, and want to explicitly mark the variable as unused. Note, that such wildcard variable name _ cannot be accessed:

let _ = someFunctionWithSideEffects(); // with type inference
let _: map<Int, Int> = emptyMap();     // with explicit type
 
dump(_); // COMPILATION ERROR! Cannot access _

return statement

The return statement ends function execution and specifies a value to be returned to the function caller.

// Simple wrapper over stdlib function now()
fun getTimeFromNow(offset: Int): Int {
    return now() + offset;
}

Block

A block statement is used to group zero or more statements. The block is delimited by a pair of braces ("curly braces", {}) and contains a list of zero or more statements and declarations.

Some statements, such as let or return, must end with a terminating semicolon ;. However, the semicolon of the last statement in the block is optional and may be omitted.

{ // <- start of the block
    // arbitrary statements:
    let value: Int = 2 + 2;
    dump(value);
} // <- end of the block
 
{ dump(2 + 2) } // a block with only one statement,
                // omitted the last and only semicolon
 
{
    let nah = 3 * 3 * 3; // a block with two statements,
    let yay = nah + 42   // but without the last semicolon
}

Expression

An expression statement is an expression used in a place where a statement is expected. The expression is evaluated and its result is discarded — therefore, it makes sense only for expressions that have side effects, such as executing a function or updating a variable.

dump(2 + 2); // stdlib function

Assignment

Assignment statements use an assignment operator (=) or augmented assignment operators (assignments combined with an operation):

let value: Int; // declaration
value = 5;      // assignment
value += 5;     // augmented assignment (one of the many, see below)
💡

Read more about assignment and augmented assignment in their dedicated section: assignment operators.

Branches

Control the flow of the code.

if...else

⚠️

Curly brackets (code blocks) are required!

When executing an if...else statement, first, the specified condition gets evaluated. If the resulting value is true, the following statement block gets executed. Otherwise, if the condition evaluates to false, the optional else block will be executed. If the else block is missing, nothing happens and execution continues further.

Regular if statement:

// condition
// ↓
if (true) { // consequence, when condition is true
    dump(2 + 2);
}

With else block:

// condition
// ↓
if (2 + 2 == 4) {
    // consequence, when condition is true
    dump(true);
} else {
    // alternative, when condition is false
    dump(false);
}

With nested if...else:

// condition
// ↓
if (2 + 2 == 3) {
    // consequence, when condition is true
    dump("3?");
//        condition2
//        ↓
} else if (2 + 2 == 4) {
    // another consequence, when condition2 is true
    dump(true);
} else {
    // alternative, when both condition and condition2 are false
    dump(false);
}
💡

Tact also has a ternary expression ?:, which is described earlier in the Book: Ternary.

try...catch

The try...catch statement is comprised of a try block and an optional catch block, which receives an Int exit code as its only argument. The code in the try block is executed first, and if it fails, the code in the catch block will be executed and changes made in try block will be rolled back, if possible.

💡

Note, that some TVM state parameters, such as codepage and gas counters, will not be rolled back. That is, all gas usage in the try block will be taken into account and the effects of opcodes that change the gas limit will be preserved.

Regular try statement:

fun braveAndTrue() {
    // Lets try and do something erroneous
    try {
        nativeThrow(42); // throwing with exit code 42
    }
 
    // The following will be executed as the erroneous code above was wrapped in a try block
    dump(42);
}

With catch (e) block:

fun niceCatch() {
    // Lets try and do something erroneous
    try {
        nativeThrow(42); // throwing with exit code 42
    } catch (err) {
        dump(err);       // this will dump the exit code caught, which is 42
    }
}

With nested try...catch:

try {
    // Preparing an x equal to 0, in such a way that Tact compiler won't realize it (yet!)
    let xs: Slice = beginCell().storeUint(0, 1).endCell().beginParse();
    let x: Int = xs.loadUint(1); // 0
 
    try {
        throw(101);     // 1. throws with exit code 101
    } catch (err) {     // 2. catches the error and captures its exit code (101) as err
        return err / x; // 3. divides err by x, which is zero, throwing with exit code 4
    }
 
} catch (err) {         // 4. catches the new error and captures its exit code (4) as err
    //   ^^^ this works without name collisions because the previous err
    //       has a different scope and is only visible inside the previous catch block
 
    dump(err);          // 5. dumps the last caught exit code (4)
}

Note, that similar to let statement, captured exit code in the catch () clause can be discarded by specifying an underscore _ in its place:

try {
    throw(42);
} catch (_) {
    dump("I don't know the exit code anymore");
}
💡

Read more about exit codes on the dedicated page: Exit codes in the Book.

Loops

Conditionally repeat certain blocks of code multiple times.

repeat

The repeat loop executes a block of code a specified number of times. Number of repetitions must be a non-negative 3232-bit Int. Otherwise an error with the exit code 5, Integer out of the expected range would be thrown.

In the following example, code inside the loop will be executed 1010 times:

let twoPow: Int = 1;
repeat (10) {  // repeat exactly 10 times
    twoPow *= 2;
}

while

The while loop continues executing the block of code as long as the given condition is true.

In the following example, the value of x is decremented by 11 on each iteration, so the loop will run 1010 times:

let x: Int = 10;
while (x > 0) {
    x -= 1;
}

do...until

The do...until loop is a post-test loop that executes the block of code at least once, and then continues to execute it until the given condition becomes true.

In the following example, the value of x is decremented by 11 on each iteration, so the loop will run 1010 times:

let x: Int = 10;
do {
    x -= 1;  // executes this code block at least once
} until (x <= 0);

foreach

The foreach loop operates on key-value pairs (entries) of map<K, V> type in sequential order: from the smallest keys of the map to the biggest ones.

This loop executes a block of code for each entry in the given map, capturing the key and value on each iteration. This is handy when you don't know in advance how many items there is in the map or don't want to explicitly look for each of the entry using .set() method of maps.

Note, that the names of captured key and value pair on each iteration are arbitrary and can be any valid Tact identifier, provided that they're new to the current scope. The most common options are: k and v, or key and value.

In the following example, map cells has 44 entries, so the loop will run 44 times:

// Empty map
let cells: map<Int, Cell> = emptyMap();
 
// Setting four entries
cells.set(1, beginCell().storeUint(100, 16).endCell());
cells.set(2, beginCell().storeUint(200, 16).endCell());
cells.set(3, beginCell().storeUint(300, 16).endCell());
cells.set(4, beginCell().storeUint(400, 16).endCell());
 
// A variable for summing up the values
let sum: Int = 0;
 
// For each key and value pair in cells map, do:
foreach (key, value in cells) { // or just k, v
    let s: Slice = value.beginParse(); // convert Cell to Slice
    sum += s.loadUint(16);             // sum the Slice values
}
dump(sum); // 1000

It's also possible to iterate over a map in contract storage, and over maps as members of instances of Struct or Message types:

import "@stdlib/deploy";
 
struct Fizz { oh_my: map<Int, Int> }
message Buzz { oh_my: map<Int, Int> }
 
contract Iterated {
    oh_my: map<Int, Int>;
 
    receive("call to iterate!") {
        let oh_my: map<Int, Int> = emptyMap();
        oh_my.set(0, 42);
        oh_my.set(1, 27);
 
        self.oh_my = oh_my; // assigning local map to the storage one
        let fizz = Fizz{ oh_my }; // field punning
        let buzz = Buzz{ oh_my }; // field punning
 
        // Iterating over map in contract storage
        foreach (key, value in self.oh_my) {
            // ...
        }
 
        // Iterating over map member of a Struct Fizz instance
        foreach (key, value in fizz.oh_my) {
            // ...
        }
 
        // Iterating over map member of a Message Buzz instance
        foreach (key, value in buzz.oh_my) {
            // ...
        }
    }
}

Note, that similar to let statement, either of captured key or value (or both) can be discarded by specifying an underscore _ in their place:

// Empty map
let quartiles: map<Int, Int> = emptyMap();
 
// Setting some entries
quartiles.set(1, 25);
quartiles.set(2, 50);
quartiles.set(3, 75);
 
// Discarding captured keys
// without modifying them in the map itself
foreach (_, value in quartiles) {}
 
// Discarding captured values
// without modifying them in the map itself
foreach (key, _ in quartiles) {}
 
// Discarding both keys and values
// without modifying them in the map itself
foreach (_, _ in quartiles) {
    // Can't access via _, but can do desired operations
    // n times, where n is the current length of the map
}
⚠️

Note, that at the moment foreach works only with explicitly provided map identifiers and nested identifier constructions, like foo.bar.targetMap or self.baz.targetMap. That is, returning a map from a function and trying to iterate over its entries won't work:

foreach (k, v in emptyMap()) {
//               ^ this will give the following error message:
//                 foreach is only allowed over maps that are path expressions,
//                 i.e. identifiers, or sequences of direct contract/struct/message accesses,
//                 like "self.foo" or "self.structure.field"
}

Trying to iterate over a map member of a Struct returned from a function also won't work, because function call is an expression and not an identifier nor a nested identifier access:

foreach (k, v in genCoolStruct().map) {
//               ^ this will give the following error message:
//                 foreach is only allowed over maps that are path expressions,
//                 i.e. identifiers, or sequences of direct contract/struct/message accesses,
//                 like "self.foo" or "self.structure.field"
}
💡

For additional loop examples see: Loops in Tact-By-Example (opens in a new tab).