Stable Variables
Stable variables persist across canister upgrades. In persistent actor, fields are stable by default; use transient var for ephemeral data that should reset on upgrade.
Stable vs Transient
When a canister is upgraded, stable variables preserve their values while transient variables reset to their initial values. This distinction determines what survives an upgrade:
| Variable Type | On Upgrade | Use Case |
|---|---|---|
| Stable field | Value preserved | Core application state |
transient | Reset to initial value | Caches, derived data, metrics |
Basic Example
In a persistent actor, all fields are stable by default. Use transient to mark ephemeral fields:
// Basic stable variable example - state persists across upgrades
persistent actor {
var counter : Nat = 0; // Implicitly stable
transient var cache : Nat = 0; // Reset on upgrade
invariant counter >= 0;
public func increment() : async Nat
modifies counter, cache
ensures counter == old(counter) + 1;
ensures result == counter;
{
counter += 1;
cache := counter * 2; // Cache is derived, no need to persist
counter
};
public query func getCached() : async Nat
reads cache
ensures result == cache;
{
cache
};
}
After an upgrade:
counterretains its value (e.g., 42 remains 42)cacheresets to 0
Explicit Stable Declarations
Verified code should use persistent actor for canister state. In a
persistent actor, fields are stable by default, so an explicit stable var is
accepted but redundant:
// Explicit stable declarations are redundant in persistent actors
persistent actor {
stable var counter : Nat = 0; // Redundant: persistent fields are stable
transient var cache : Nat = 0; // Explicitly transient
invariant counter >= 0;
public func increment() : async Nat
modifies counter, cache
ensures counter == old(counter) + 1;
ensures result == counter;
{
counter += 1;
cache := counter * 2;
counter
};
}
Prefer omitting the redundant stable keyword unless you are documenting a
migration from older Motoko code.
Stable Type Requirements
Not all types can be stable. A type must be serializable to persist across upgrades.
Allowed Types
// Stable types - only serializable types can be stable
persistent actor {
// Allowed stable types
var count : Nat = 0; // Primitives
var balance : Int = 100;
var active : Bool = true;
var name : Text = "Account";
var data : [Nat] = [1, 2, 3]; // Immutable arrays
var items : [var Nat] = [var 1, 2, 3]; // Mutable arrays
var position : { x : Int; y : Int } = { x = 0; y = 0 }; // Records
var status : { #active; #pending; #done } = #active; // Variants
var maybe : ?Nat = ?42; // Options
public func getCount() : async Nat reads count {
count
};
}
Stable types include:
- Primitives:
Nat,Int,Bool,Text,Char,Blob,Principal - Fixed-width integers:
Nat8-Nat64,Int8-Int64 - Arrays: Both
[T](immutable) and[var T](mutable) - Records:
{ field1 : T1; field2 : T2 }with stable field types - Variants:
{ #case1; #case2 : T }with stable payload types - Options:
?Twhere T is stable - Tuples:
(T1, T2)where all elements are stable
Disallowed Types
Certain types cannot be stable because they cannot be serialized:
// Non-stable types are rejected by the type checker
persistent actor {
// ERROR: Async types cannot be stable
var pending : async Nat = async { 42 };
}
// Error: variable pending is declared stable but has non-stable type
Non-stable types include:
- Async types:
async Tandasync* T(pending futures or computations) - Local functions: Functions not marked
shared - Error type:
Error - Modules: Module types cannot be persisted
The compiler reports error M0131 when a stable variable has a non-stable type.
Verification with Stable Fields
For ordinary method verification, stable and transient fields behave identically. Stability controls upgrade persistence; it does not change reads, modifies, invariants, or await interference. Both stable and transient fields require explicit footprints:
// Stable and transient fields in verification
persistent actor {
var balance : Int = 100; // Stable - main state
transient var lastRead : Int = 0; // Transient - cached value
invariant balance >= 0;
public func withdraw(amount : Int) : async Bool
modifies balance, lastRead
entry_requires amount > 0;
requires amount > 0;
ensures result == true ==> balance == old(balance) - amount;
ensures result == false ==> balance == old(balance);
ensures balance >= 0;
{
if (balance >= amount) {
balance -= amount;
lastRead := 0;
true
} else {
false
}
};
public query func getBalance() : async Int
reads balance
modifies lastRead
ensures result == balance;
{
lastRead := balance;
balance
};
}
Key points:
- Use
modifiesfor any field you write to (stable or transient) - Use
readsfor read-only access - Actor invariants apply to all fields regardless of stability
- The
old()expression captures method-entry values for both types - Across a suspending
await, both stable and transient actor fields are treated as shared state; only invariants and local snapshots survive
When to Use Each
// Choosing between stable and transient fields
persistent actor {
// STABLE (default): Core application state
var users : Nat = 0;
var totalBalance : Nat = 0;
var ownerId : Nat = 0;
// TRANSIENT: Derived or reconstructible data
transient var userCount : Nat = 0; // Can recompute from users
transient var lastActivity : Nat = 0; // Ephemeral metric
invariant userCount <= users;
public func addUser(_id : Nat, _name : Text) : async ()
modifies users, userCount
ensures users == old(users) + 1;
{
users += 1;
userCount := users;
};
// Called after upgrade to restore transient state
public func rebuildCache() : async ()
modifies userCount
reads users
ensures userCount == users;
{
userCount := users;
};
public query func snapshot() : async (Nat, Nat, Nat, Nat)
reads users, totalBalance, ownerId, lastActivity
ensures result.0 == users;
ensures result.1 == totalBalance;
ensures result.2 == ownerId;
ensures result.3 == lastActivity;
{
(users, totalBalance, ownerId, lastActivity)
};
}
Use Stable For
- User data: Accounts, balances, records
- Configuration: Settings, parameters, admin principals
- Core state: Anything that defines the application's state
Use Transient For
- Caches: Computed values that can be reconstructed
- Metrics: Counters and statistics that reset each deployment
- Session state: Temporary data for ongoing operations
- Derived data: Values computed from stable state
Upgrade Considerations
When upgrading an actor, the runtime checks type compatibility between the old and new versions:
| Change | Compatibility |
|---|---|
| Add new stable field | Safe (initialized to default) |
| Remove stable field | Requires migration function |
| Change field type | Must be subtype-compatible |
| Rename stable field | Requires migration function |
If types are incompatible, the upgrade fails with an error. Use migration functions to transform data between versions.
Error Messages
| Code | Message | Cause |
|---|---|---|
| M0131 | Variable declared stable but has non-stable type | Using async/async*, Error, module, or local function type |
| M0132 | Misplaced stability declaration | Using stable on a non-actor field |
| M0133 | Misplaced stability modifier | Using stable on non-variable declarations |
| M0169 | Stable variable cannot be discarded | Removing stable field without migration |
| M0218 | Redundant stable keyword | Using stable in persistent actor (implicit) |
Summary
stable varpreserves values across upgrades;transient varresets- In
persistent actor, fields are stable by default - In legacy actor mode, fields are transient by default
- Types must be serializable to be stable
- Verification treats stable and transient fields identically
- Use stable for core state; use transient for caches and derived data
Related Topics
- Orthogonal persistence for method-call and commit behavior
- Upgrade compatibility for stable subtyping
- Actor fields for
var,let, andtransient - Async commit points for rollback behavior