Near Learn

Cross-contract calls

The biggest mental shift: calls to other contracts are asynchronous. You schedule the call and a callback receives the result in a later step — no inline return value.

Beginner5 min read3-question check

This is the largest difference from the EVM. In Solidity, calling another contract is synchronous — you get the return value on the next line. On NEAR every cross-contract call is asynchronous: you schedule it, your method ends, and a separate callback method receives the result in a later block.

Read another contract’s value
Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

interface ICounter {
    function getCount() external view returns (uint256);
}

contract Caller {
    uint256 public lastSeen;

    function fetch(address counter) external {
        // synchronous: the value is available immediately
        lastSeen = ICounter(counter).getCount();
    }
}
Rust
use near_sdk::{near, env, ext_contract, AccountId, Gas, Promise, PromiseError};

// the interface of the contract we want to call
#[ext_contract(ext_counter)]
trait Counter {
    fn get_count(&self) -> u64;
}

#[near(contract_state)]
#[derive(Default)]
pub struct Caller {
    last_seen: u64,
}

#[near]
impl Caller {
    // &mut self: creating promises is a change call, never a view
    pub fn fetch(&mut self, counter: AccountId) -> Promise {
        // schedule the call, then a callback to handle its result
        ext_counter::ext(counter)
            .with_static_gas(Gas::from_tgas(5))
            .get_count()
            .then(
                Self::ext(env::current_account_id())
                    .with_static_gas(Gas::from_tgas(5))
                    .on_fetched(),
            )
    }

    #[private] // only this contract may call its own callback
    pub fn on_fetched(&mut self, #[callback_result] res: Result<u64, PromiseError>) -> u64 {
        match res {
            Ok(count) => {
                self.last_seen = count;
                count
            }
            Err(_) => self.last_seen, // the call failed — decide what to do
        }
    }
}
Coming from EVM

A Solidity interface + synchronous call ↔ #[ext_contract] + a scheduled Promise + a callback. The value is not available inline — fetch returns a Promise, and on_fetched gets the result later.

Scheduling promises is only allowed in a change call: fetch takes &mut self and must be sent as a transaction (near call, with gas attached). Even though it only *reads* the other contract, a free near view call cannot create promises and would fail as "prohibited in view".

The callback is marked #[private] so only the contract itself can invoke it, and it takes #[callback_result] res: Result<T, PromiseError> — you must handle the failure case, because the other contract may panic or run out of gas.

You attach a gas budget to each leg with .with_static_gas(..). Because there is no synchronous return, the reentrancy shape is different too: state you changed before scheduling is already committed when the callback runs.

Check yourself

3 questions · progress saved in this browser

  1. 1.In Solidity, ICounter(c).getCount() gives you the value on the next line. How do you get another contract's return value on NEAR?
  2. 2.Why is the callback on_fetched marked #[private]?
  3. 3.Why does the callback take #[callback_result] res: Result<u64, PromiseError>?