Near Learn

Ordering and timing guarantees

What NEAR does and does not order: actions, chains, joins, cross-shard and delayed receipts, refunds and block times, and why “next block” is a bad bet.

Advanced8 min read3-question check

Asynchronous code goes wrong when it silently relies on an order nobody promised. On NEAR the list of real guarantees is short, and it is worth knowing by heart. Everything outside that list should be treated as “can happen in any order, possibly blocks apart”.

What is guaranteed#

GuaranteeWhat it means in practice
Actions inside one receipt run in order, atomicallycreate_account → transfer → deploy → function_call is safe to depend on (see batches)
A method runs to completion without interruptionNo other code touches your contract’s state while your method executes. There is no mid-method re-entry
a.then(b): b runs after a has finishedWhether a succeeded or failed, b sees its final result
A joint .then waits for every memberThe callback runs after the slowest member, with results in join order
Each receipt sees everything committed before it on that accountReceipts for one account execute one at a time on its shard, so state reads are never torn

What is not guaranteed#

  • Order between independent promises. Two promises you create in the same method, without a .then between them, are independent receipts. Do not build logic that needs one to land first.
  • Order between your receipts and other users’ transactions. Between your call and your callback, any number of unrelated transactions can execute against your contract. That is the reentrancy-across-receipts window.
  • “The next block”. The best case is one block per hop, but a busy shard puts incoming receipts into a delayed receipts queue and works through it in later blocks. Cross-shard congestion control (NEP-539) can also slow down or refuse new work aimed at a congested shard.
  • Refunds before callbacks. Refunds of gas and of failed deposits are separate receipts from the system account. Your callback may run before or after them.
  • Transaction order across signers. Nonces order transactions from one access key, and only their inclusion. They say nothing about when the receipts those transactions spawn will finish.

Time is per receipt#

env::block_height() and env::block_timestamp() return the values of the block in which the current receipt executes. A method and its callback therefore see different times. Anything time-based that you check before the call (an auction deadline, a price’s freshness, a vesting cliff) may no longer hold when the callback runs.

Re-check deadlines in the callback, and decide what a late result means
Rust
use near_sdk::json_types::U128;
use near_sdk::{env, log, near, require, AccountId, Gas, Promise, PromiseError};

// Auction state, ext_escrow and the lock/unlock/record helpers are defined elsewhere.
#[near]
impl Auction {
    pub fn bid_with_tokens(&mut self, amount: U128) -> Promise {
        // first check: is the auction still open NOW?
        require!(env::block_timestamp() < self.ends_at_ns, "auction closed");
        let bidder = env::predecessor_account_id();
        self.lock_tokens(&bidder, amount.0); // effects first
        ext_escrow::ext(self.escrow.clone())
            .with_static_gas(Gas::from_tgas(10))
            .hold(bidder.clone(), amount)
            .then(
                Self::ext(env::current_account_id())
                    .with_static_gas(Gas::from_tgas(10))
                    .on_held(bidder, amount),
            )
    }

    #[private]
    pub fn on_held(
        &mut self,
        bidder: AccountId,
        amount: U128,
        #[callback_result] r: Result<(), PromiseError>,
    ) -> bool {
        // second check: blocks have passed since bid_with_tokens ran
        let late = env::block_timestamp() >= self.ends_at_ns;
        if r.is_err() || late {
            log!("bid by {} rejected (failed: {}, late: {})", bidder, r.is_err(), late);
            self.unlock_tokens(&bidder, amount.0);
            // if the hold succeeded but we are late, also undo it on the escrow
            if r.is_ok() {
                ext_escrow::ext(self.escrow.clone())
                    .with_static_gas(Gas::from_tgas(10))
                    .release(bidder, amount)
                    .detach();
            }
            return false;
        }
        self.record_bid(bidder, amount.0);
        true
    }
}

Ordering across shards#

Every account lives on exactly one shard, and its receipts execute there. A receipt for an account on another shard is emitted as an outgoing receipt and picked up by the receiving shard in a later chunk. Two contracts that call each other back and forth may sit on different shards, and their receipts take turns across the boundary, one hop at a time.

Shard assignment is a protocol detail that changes with resharding, so it is not something your contract should know or depend on. The safe assumption is simply that every hop can add latency and that no two hops are synchronised.

Two users, one contract, interleaved receipts
Shell
block N+1  vault: alice.withdraw(50)    balance 100 -> 50, calls token
block N+1  vault: bob.deposit(10)       independent tx in the same block (before or after alice)
block N+2  token: ft_transfer(alice)    fails: alice is not registered on the token
block N+2  vault: carol.withdraw(5)     another unrelated tx
block N+3  vault: on_withdraw(alice)    sees failure; restores +50 to the CURRENT balance
block N+3  vault: refund receipt        the 1 yoctoNEAR deposit comes back (any order vs on_withdraw)

Check yourself

3 questions · progress saved in this browser

  1. 1.Your method creates two promises, one to a.near and one to b.near, without .then between them. Which statement is safe?
  2. 2.A method checks env::block_timestamp() < deadline, then makes a call with a callback. Why check the deadline again in the callback?
  3. 3.Under heavy load, what happens to receipts that do not fit into a shard’s chunk?