Async Model
The promise system is the foundation of NEAR's asynchronous execution model. When a smart contract needs to call another contract, it creates promises - abstract representations of future computations that will be converted to receipts for actual execution.
Understanding Promises
In NEAR, a Promise is a placeholder for work that will happen in a future block. Unlike JavaScript promises or Rust futures, NEAR promises are not in-memory objects that you await - they're instructions that get converted to receipts when the current function returns.
The Key Principle
Promises are declarative, not imperative. You're not calling another contract - you're declaring that you want to call it. The actual execution happens in future blocks, potentially on different shards.
This is why:
- You can't get a return value immediately
- You must use callbacks to process results
- The callback might execute in a completely different block/shard
- NEAR is fundamentally asynchronous
The Promise DAG
Promises form a Directed Acyclic Graph (DAG) of dependencies:
Promise Creation Functions
promise_create()
Creates a new promise for calling another account's function.
pub fn promise_create(
&mut self,
account_id_len: u64,
account_id_ptr: u64,
function_name_len: u64,
function_name_ptr: u64,
arguments_len: u64,
arguments_ptr: u64,
amount: u128,
gas: u64,
) -> Result<u64>
What it does:
- Reads target
account_idandfunction_namefrom WASM memory - Reads
arguments(the call payload) - Creates a new receipt destined for the target account
- Attaches the specified
amountof NEAR and prepaysgas - Returns a
PromiseIndexthat can be used in subsequent calls
Usage pattern:
// In contract code
let promise_id = env::promise_create(
"other-contract.near",
"some_method",
b'{"arg": "value"}',
0, // attached NEAR
5_000_000_000_000, // attached gas (5 TGas)
);
promise_then()
Creates a callback that executes after another promise completes.
pub fn promise_then(
&mut self,
promise_idx: u64,
// ... target account, method, args ...
amount: u128,
gas: u64,
) -> Result<u64>
What it does:
- Takes an existing
promise_idxas a dependency - Creates a new receipt that waits for the first promise to complete
- When the first promise finishes, its return value becomes available as
PromiseResultin the callback - Returns a new
PromiseIndexfor the callback
The Key Insight: The callback doesn't execute immediately. NEAR creates two receipts:
- The original promise (call to other contract)
- A callback receipt with an
input_data_iddependency
When the first receipt executes and produces a result, that result becomes a DataReceipt that satisfies the callback's dependency.
promise_and()
Joins multiple promises - waits for ALL of them to complete.
pub fn promise_and(
&mut self,
promise_idx_ptr: u64,
promise_idx_count: u64,
) -> Result<u64>
Use case: When you need to call multiple contracts and wait for all results:
let p1 = env::promise_create("contract_a", "get_data", ...);
let p2 = env::promise_create("contract_b", "get_data", ...);
let p3 = env::promise_create("contract_c", "get_data", ...);
let all = env::promise_and(&[p1, p2, p3]);
let callback = env::promise_then(all, "self", "process_all_results", ...);
promise_batch_create() and promise_batch_then()
Low-level functions for creating receipts with multiple actions:
// Create an account with initial balance
let batch = env::promise_batch_create("new_account.near");
env::promise_batch_action_create_account(batch);
env::promise_batch_action_transfer(batch, deposit_amount);
env::promise_batch_action_add_full_access_key(batch, public_key);
Promise Results
When a callback executes, it can read the results of the promises it depends on.
pub enum PromiseResult {
/// The promise has not yet been resolved
Pending,
/// The promise succeeded with a value
Successful(Vec<u8>),
/// The promise failed (no value)
Failed,
}
In callback code:
fn callback() {
let count = env::promise_results_count();
for i in 0..count {
match env::promise_result(i, 0) {
PromiseResult::Successful(value) => {
// Process successful result
}
PromiseResult::Failed => {
// Handle failure
}
PromiseResult::Pending => unreachable!(),
}
}
}
Receipt Types
Receipts are the fundamental unit of execution in NEAR. While transactions are what users submit, receipts are what the runtime actually processes.
Receipt Hierarchy
pub enum ReceiptEnum {
Action(ActionReceipt) = 0, // Execute actions
Data(DataReceipt) = 1, // Deliver return data
PromiseYield(ActionReceipt) = 2, // Suspend execution (NEP-519)
PromiseResume(DataReceipt) = 3, // Resume suspended execution
GlobalContractDistribution(...) = 4, // Distribute shared contracts
ActionV2(ActionReceiptV2) = 5, // Enhanced action receipt
PromiseYieldV2(ActionReceiptV2) = 6, // Yield with enhanced receipt
}
ActionReceipt: The Workhorse
ActionReceipts contain the actual work to be done.
pub struct ActionReceipt {
/// Account that originally signed the transaction
pub signer_id: AccountId,
/// Public key used for signing (for gas refunds)
pub signer_public_key: PublicKey,
/// Gas price when this receipt was created
pub gas_price: Balance,
/// Where to send the result of this execution
pub output_data_receivers: Vec<DataReceiver>,
/// What this receipt is waiting for (callback dependencies)
pub input_data_ids: Vec<CryptoHash>,
/// The actual operations to perform
pub actions: Vec<Action>,
}
Key Fields:
| Field | Purpose |
|---|---|
signer_id | Track back to original tx signer for gas refunds |
gas_price | Gas price at creation time (for deterministic refunds) |
output_data_receivers | Receipts waiting for this execution's result |
input_data_ids | Dependencies this receipt must wait for |
actions | Operations to perform: FunctionCall, Transfer, etc. |
DataReceipt: The Messenger
DataReceipts carry return values between receipts.
pub struct DataReceipt {
/// Unique identifier matching an input_data_id somewhere
pub data_id: CryptoHash,
/// The actual return value (None means execution failed)
pub data: Option<Vec<u8>>,
}
The Data Flow Dance
Receipt Lifecycle
Stage 1: Creation
Receipts are created in two ways:
- From transactions: The first receipt for any transaction
- From execution: When a contract creates promises
Stage 2: Routing
Each receipt routes to its receiver's shard:
pub fn receiver_shard_id(&self, shard_layout: &ShardLayout) -> ShardId {
shard_layout.account_id_to_shard_id(self.receiver_id())
}
Stage 3: Waiting (if needed)
If a receipt has input_data_ids, it cannot execute until ALL dependencies are satisfied. These become delayed receipts.
Stage 4: Execution
When all dependencies are satisfied:
- Load input data from state as
PromiseResults - Delete input data from state (one-time consumption)
- Execute actions sequentially
- Produce outputs (DataReceipts, new ActionReceipts, refunds)
Cross-Shard Communication
NEAR's sharding model means different accounts live on different shards. Cross-shard communication uses asynchronous receipt passing.
Account-to-Shard Mapping
Every account belongs to exactly one shard, determined by alphabetical ordering against boundary accounts:
boundary_accounts: ["aurora", "aurora-0", "kkuuue2akv_1630967379.near"]
Shard 0: accounts < "aurora"
Shard 1: "aurora" <= accounts < "aurora-0"
Shard 2: "aurora-0" <= accounts < "kkuuue2akv_1630967379.near"
Shard 3: accounts >= "kkuuue2akv_1630967379.near"
Alphabetical ordering uses the full account name. So a.near and z.a.near may be on different shards!
Cross-Shard Receipt Propagation
Execution Order Guarantees
Within a Shard:
- Transactions execute in chunk order
- Receipts execute deterministically (local first, then by receipt_id)
Across Shards:
- No global ordering guarantee across shards
- Causal ordering: If receipt A creates receipt B, A always executes before B
- Data dependencies: A receipt waits until ALL its
input_data_idsare satisfied - No double execution: Each receipt executes exactly once
Delayed Receipts
When a receipt arrives but its dependencies aren't ready, it becomes a delayed receipt.
Why Delayed?
- Cross-shard calls take time (at least 1 block per hop)
- Callback can't execute until source call completes
- Network latency and shard processing order affect arrival time
Processing (each block):
- Check delayed receipts queue
- For each, check if dependencies arrived
- If satisfied, promote to execution
- If not, leave in queue
Congestion Control
Too many cross-shard receipts can overwhelm a shard. NEAR implements backpressure (NEP-539):
- Memory-based limiting: Reject new transactions when shard memory exceeds threshold (~500MB)
- Receiver-specific backpressure: Stop accepting transactions to congested accounts
- Deadlock prevention: Always allow minimum throughput to drain queues
Practical Example: Cross-Contract Call with Callback
Step 1: Contract A Creates Promises
Contract A executes transaction:
├── promise_create("B", "getData", ...) → Promise 0
└── promise_then(0, "A", "callback", ...) → Promise 1
Step 2: ReceiptManager Generates Receipts
Receipt R1 (for Promise 0):
├── receiver_id: "B"
├── actions: [FunctionCall("getData")]
├── input_data_ids: [] (no dependencies)
└── output_data_receivers: [{data_id: D1, receiver_id: "A"}]
Receipt R2 (for Promise 1):
├── receiver_id: "A"
├── actions: [FunctionCall("callback")]
├── input_data_ids: [D1] (waits for R1's output)
└── output_data_receivers: []
Step 3: Execution Timeline
Block N (Shard A):
└── Transaction executes → creates R1, R2
└── R1 sent to Shard B, R2 stored as delayed (waiting for D1)
Block N+1 (Shard B):
└── R1 executes → produces DataReceipt D1 with result
└── D1 sent to Shard A
Block N+2 (Shard A):
└── D1 arrives → satisfies R2's dependency
└── R2 executes with D1's data as PromiseResult::Successful
Gas Weight Distribution
When you don't know exactly how much gas to allocate, use gas weights:
env::promise_batch_action_function_call_weight(
promise_id,
"method",
args,
deposit,
Gas::ZERO, // minimum gas
GasWeight(1), // weight for distributing remaining gas
);
The ReceiptManager distributes unused gas proportionally based on weights.