Motoko Language
What This Is
Motoko is the native programming language for Internet Computer canisters. It has actor-based concurrency, built-in persistence (orthogonal persistence), and a type system designed for safe canister upgrades. This skill covers the syntax and type system pitfalls that cause compilation errors or runtime traps when generating Motoko code.
Prerequisites
mops.toml at the project root:
[toolchain]
moc = "1.3.0"
[dependencies]
core = "2.3.1"
moc must be pinned — the @dfinity/motoko recipe resolves the compiler from this field. Without it, icp build fails. Install the package manager with npm i -g ic-mops.
Compilation Error Pitfalls
-
Writing
actorinstead ofpersistent actor. Since moc 0.15.0, thepersistentkeyword is mandatory on all actors and actor classes. Plainactorproduces error M0220.// Wrong — error M0220: this actor or actor class should be declared `persistent` actor { var count : Nat = 0; }; // Correct persistent actor { var count : Nat = 0; }; // Actor classes also require it persistent actor class Counter(init : Nat) { var count : Nat = init; };Migrating from Caffeine: The Caffeine platform used a moc fork with
--default-persistent-actors, makingpersistentoptional and all variables implicitly stable. On standard moc, addpersistentto every actor and usetransient varfor variables that should reset on upgrade (see pitfall #3). -
Putting type declarations before the actor. Only
importstatements are allowed beforepersistent actor. All type definitions,let, andvardeclarations must go inside the actor body. Violation produces error M0141.// Wrong — error M0141: move these declarations into the body of the main actor or actor class import Nat "mo:core/Nat"; type UserId = Nat; let MAX = 100; persistent actor { }; // Correct import Nat "mo:core/Nat"; persistent actor { type UserId = Nat; let MAX = 100; }; -
Using
stablekeyword in persistent actors. Inpersistent actor, allletandvardeclarations are stable by default. Writingstable varis redundant and produces warning M0218. Usetransient varfor data that should reset on upgrade.persistent actor { // Wrong — warning M0218: redundant `stable` keyword stable var count : Nat = 0; // Correct — implicitly stable var count : Nat = 0; // Correct — resets to 0 on every upgrade transient var requestCount : Nat = 0; };The old keyword
flexiblewas renamed totransientin moc 0.13.5. Never useflexible. -
Using HashMap, Buffer, TrieMap, or RBTree in persistent actors. These types from the old
baselibrary contain closures and are NOT stable. Using them in apersistent actorproduces error M0131. They do not exist inmo:core— the modern standard library has stable replacements.// Wrong — these types do not exist in mo:core and are not stable import HashMap "mo:base/HashMap"; import Buffer "mo:base/Buffer"; // Correct — use mo:core stable collections import Map "mo:core/Map"; // key-value map (B-tree, stable) import Set "mo:core/Set"; // set (B-tree, stable) import List "mo:core/List"; // growable list (stable) import Queue "mo:core/Queue"; // FIFO queue (stable) -
Reassigning
letbindings.letis immutable in Motoko — there is no reassignment. Usevarfor mutable values.// Wrong — cannot assign to immutable let binding let count = 0; count := 1; // Correct var count = 0; count := 1; // Also correct — let is fine for collections (they mutate internally) let users = Map.empty<Nat, Text>(); Map.add(users, Nat.compare, 0, "Alice"); // mutates the map in place -
Using
continueorbreakwithout labels (moc < 1.2.0). Unlabeledbreakandcontinuein loops require moc >= 1.2.0. For older compilers, or when targeting an outer loop, use labeled loops.// Works since moc 1.2.0 for (x in items.vals()) { if (x == 0) continue; }; // Required for moc < 1.2.0, or to target a specific loop label outer for (x in items.vals()) { label inner for (y in other.vals()) { if (y == 0) continue inner; if (x == y) break outer; }; }; -
Shared function parameter types must be shared. All parameters and return types of
publicactor functions must be shared types. Closures, mutable records,Error, andasync*are NOT shared types. Error codes: M0031, M0032, M0033.// Wrong — functions are not shared types public func register(callback : () -> ()) : async () { }; // Wrong — mutable records are not shared types public func store(data : { var count : Nat }) : async () { }; // Correct — use immutable records and avoid closures public func store(data : { count : Nat }) : async () { }; -
Incomplete pattern matches. Switch expressions must cover all possible values. Missing cases produce error M0145.
type Color = { #red; #green; #blue }; // Wrong — error M0145: pattern does not cover value #blue func name(c : Color) : Text { switch (c) { case (#red) "Red"; case (#green) "Green"; } }; // Correct — cover all cases (or use a wildcard) func name(c : Color) : Text { switch (c) { case (#red) "Red"; case (#green) "Green"; case (#blue) "Blue"; } }; -
Variant tag argument precedence. Variant constructors with arguments bind tightly. When passing a complex expression, use parentheses to avoid unexpected parsing.
type Action = { #transfer : Nat; #none }; // Potentially confusing — what does this parse as? let a = #transfer 1 + 2; // parsed as (#transfer(1)) + 2 — type error // Clear — use parentheses for complex expressions let a = #transfer(1 + 2); // #transfer(3) -
Using
do ? { }blocks incorrectly. The!operator (null break) only works inside ado ? { }block. Using it outside produces error M0064.// Wrong — error M0064: misplaced '!' (no enclosing 'do ? { ... }' expression) func getName(map : Map.Map<Nat, Text>, id : Nat) : ?Text { let name = Map.get(map, Nat.compare, id)!; ?name }; // Correct — wrap in do ? { } func getName(map : Map.Map<Nat, Text>, id : Nat) : ?Text { do ? { let name = Map.get(map, Nat.compare, id)!; name } };
mo:core Standard Library
The core library (package name core on mops) is the modern standard library. It replaces the deprecated base library. Minimum moc version: 1.0.0.
Import pattern
Always import from mo:core/, never from mo:base/:
import Map "mo:core/Map";
import Set "mo:core/Set";
import List "mo:core/List";
import Nat "mo:core/Nat";
import Text "mo:core/Text";
import Int "mo:core/Int";
import Option "mo:core/Option";
import Result "mo:core/Result";
import Iter "mo:core/Iter";
import Principal "mo:core/Principal";
import Time "mo:core/Time";
import Debug "mo:core/Debug";
import Runtime "mo:core/Runtime";
Available modules
Array, Base64, Blob, Bool, CertifiedData, Char, Cycles, Debug, Error, Float, Func, Int, Int8, Int16, Int32, Int64, InternetComputer, Iter, List, Map, Nat, Nat8, Nat16, Nat32, Nat64, Option, Order, Principal, PriorityQueue, Queue, Random, Region, Result, Runtime, Set, Stack, Text, Time, Timer, Tuples, Types, VarArray, WeakReference.
Key collection APIs
Map (B-tree, O(log n), stable):
import Map "mo:core/Map";
import Nat "mo:core/Nat";
persistent actor {
let users = Map.empty<Nat, Text>();
public func addUser(id : Nat, name : Text) : async () {
Map.add(users, Nat.compare, id, name);
};
public query func getUser(id : Nat) : async ?Text {
Map.get(users, Nat.compare, id)
};
public query func userCount() : async Nat {
Map.size(users)
};
public func removeUser(id : Nat) : async () {
Map.remove(users, Nat.compare, id);
};
};
Set (B-tree, O(log n), stable):
import Set "mo:core/Set";
import Text "mo:core/Text";
let tags = Set.empty<Text>();
Set.add(tags, Text.compare, "motoko");
Set.contains(tags, Text.compare, "motoko"); // true
List (growable, stable):
import List "mo:core/List";
let items = List.empty<Text>();
List.add(items, "first");
List.add(items, "second");
List.get(items, 0); // ?"first" — returns ?T, null if out of bounds
List.at(items, 0); // "first" — returns T, traps if out of bounds
Note: List.get returns ?T (safe). List.at returns T and traps on out-of-bounds. In core < 1.0.0 the names were different (get was getOpt, at was get).
Text.join parameter order
import Text "mo:core/Text";
// First parameter is the iterator, second is the separator
let result = Text.join(["a", "b", "c"].vals(), ", "); // "a, b, c"
Type parameters often need explicit annotation
Invariant type parameters cannot always be inferred. When the compiler says “add explicit type instantiation”, provide type arguments:
import VarArray "mo:core/VarArray";
// May fail type inference
let doubled = VarArray.map(arr, func x = x * 2);
// Fix: add explicit type arguments
let doubled = VarArray.map<Nat, Nat>(arr, func x = x * 2);
Common Patterns
Actor with Map and query methods
import Map "mo:core/Map";
import Nat "mo:core/Nat";
import Text "mo:core/Text";
import Time "mo:core/Time";
import Iter "mo:core/Iter";
persistent actor {
type Profile = {
name : Text;
bio : Text;
created : Int;
};
let profiles = Map.empty<Nat, Profile>();
var nextId : Nat = 0;
public func createProfile(name : Text, bio : Text) : async Nat {
let id = nextId;
nextId += 1;
Map.add(profiles, Nat.compare, id, {
name;
bio;
created = Time.now();
});
id
};
public query func getProfile(id : Nat) : async ?Profile {
Map.get(profiles, Nat.compare, id)
};
public query func listProfiles() : async [(Nat, Profile)] {
Iter.toArray(Map.entries(profiles))
};
};
Option handling with switch
public query func greetUser(id : Nat) : async Text {
switch (Map.get(profiles, Nat.compare, id)) {
case (?profile) { "Hello, " # profile.name # "!" };
case null { "User not found" };
}
};
Error handling with try/catch
import Error "mo:core/Error";
// try/catch only works with inter-canister calls (async contexts)
public func safeTransfer(to : Principal, amount : Nat) : async Result.Result<(), Text> {
try {
await remoteCanister.transfer(to, amount);
#ok()
} catch (e) {
#err(Error.message(e))
}
};
Reading canister environment variables
import Runtime "mo:core/Runtime";
// Injected by icp deploy — available in mo:core >= 2.1.0
// Returns ?Text — null if the variable is not set
let ?backendId = Runtime.envVar("PUBLIC_CANISTER_ID:backend")
else Debug.trap("PUBLIC_CANISTER_ID:backend not set");