Skip to main content

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 TypeOn UpgradeUse Case
Stable fieldValue preservedCore application state
transientReset to initial valueCaches, derived data, metrics

Basic Example​

In a persistent actor, all fields are stable by default. Use transient to mark ephemeral fields:

stable-basic.sr9
// 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:

  • counter retains its value (e.g., 42 remains 42)
  • cache resets 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:

stable-explicit.sr9
// 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.sr9
// 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: ?T where T is stable
  • Tuples: (T1, T2) where all elements are stable

Disallowed Types​

Certain types cannot be stable because they cannot be serialized:

stable-nonstable-type_reject.sr9
// 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 T and async* 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-verification.sr9
// 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 modifies for any field you write to (stable or transient)
  • Use reads for 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​

stable-when-to-use.sr9
// 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:

ChangeCompatibility
Add new stable fieldSafe (initialized to default)
Remove stable fieldRequires migration function
Change field typeMust be subtype-compatible
Rename stable fieldRequires migration function

If types are incompatible, the upgrade fails with an error. Use migration functions to transform data between versions.

Error Messages​

CodeMessageCause
M0131Variable declared stable but has non-stable typeUsing async/async*, Error, module, or local function type
M0132Misplaced stability declarationUsing stable on a non-actor field
M0133Misplaced stability modifierUsing stable on non-variable declarations
M0169Stable variable cannot be discardedRemoving stable field without migration
M0218Redundant stable keywordUsing stable in persistent actor (implicit)

Summary​

  • stable var preserves values across upgrades; transient var resets
  • 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