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