Skip to main content
NEAR contracts can interact with other deployed contracts, querying information and executing functions through cross-contract calls. Since NEAR is a sharded blockchain, cross-contract calls are asynchronous and independent.
Asynchronous ExecutionThe calling function and the callback execute in different blocks (typically 1-2 blocks apart). During this time, the contract remains active and can receive other calls.
Independent TransactionsEach function executes in its own context. If the external call fails, the calling function has already completed successfully - there’s no automatic rollback. You must handle failures explicitly in the callback.

Querying Information

Query information from another contract with a cross-contract call:
use near_sdk::{ext_contract, near, Gas, Promise};

// Define the interface for the external contract
#[ext_contract(hello_near)]
pub trait HelloNear {
    fn get_greeting(&self) -> String;
}

#[near]
impl Contract {
    pub fn query_greeting(&self) -> Promise {
        let hello_account = "hello-near.testnet".parse().unwrap();

        hello_near::ext(hello_account)
            .with_static_gas(Gas::from_tgas(5))
            .get_greeting()
            .then(
                Self::ext(env::current_account_id())
                    .with_static_gas(Gas::from_tgas(5))
                    .query_greeting_callback()
            )
    }

    #[private]
    pub fn query_greeting_callback(&self, #[callback_result] result: Result<String, PromiseError>) -> String {
        match result {
            Ok(greeting) => {
                near_sdk::log!("Got greeting: {}", greeting);
                greeting
            },
            Err(_) => "Failed to get greeting".to_string(),
        }
    }
}
The high-level API uses #[ext_contract] to define external contract interfaces.

Sending Information

Call another contract passing information:
use near_sdk::{ext_contract, near, Gas, Promise};

#[ext_contract(hello_near)]
pub trait HelloNear {
    fn set_greeting(&mut self, message: String);
}

#[near]
impl Contract {
    pub fn change_greeting(&mut self, new_greeting: String) -> Promise {
        let hello_account = "hello-near.testnet".parse().unwrap();

        hello_near::ext(hello_account)
            .with_static_gas(Gas::from_tgas(5))
            .set_greeting(new_greeting.clone())
            .then(
                Self::ext(env::current_account_id())
                    .with_static_gas(Gas::from_tgas(5))
                    .change_greeting_callback(new_greeting)
            )
    }

    #[private]
    pub fn change_greeting_callback(
        &mut self,
        new_greeting: String,
        #[callback_result] result: Result<(), PromiseError>
    ) -> String {
        match result {
            Ok(_) => {
                near_sdk::log!("Successfully changed greeting to: {}", new_greeting);
                format!("Success: {}", new_greeting)
            },
            Err(_) => {
                near_sdk::log!("Failed to change greeting");
                "Failed".to_string()
            },
        }
    }
}

Understanding Promises

Cross-contract calls work by creating promises:
  1. Promise to execute code in external contract - Promise.create
  2. Optional callback promise - Promise.then
Both promises contain:
  • Address of the contract to call
  • Function name to execute
  • Arguments to pass
  • Amount of GAS to use
  • NEAR deposit to attach
use near_sdk::{near, env, Promise, Gas, NearToken};

#[near]
impl Contract {
    pub fn cross_contract_call(&self) -> Promise {
        let external_address = "external.near".parse().unwrap();
        let args = json!({ "param": "value" }).to_string().into_bytes();

        Promise::new(external_address)
            .function_call(
                "function_name".to_owned(),
                args,
                NearToken::from_near(0),  // Attached deposit
                Gas::from_tgas(5),        // Gas for external call
            )
            .then(
                Self::ext(env::current_account_id())
                    .with_static_gas(Gas::from_tgas(5))
                    .callback_name()
            )
    }

    #[private]
    pub fn callback_name(&self) {
        // Handle callback
    }
}
The callback can be made to any contract, meaning the result could be handled by another contract.

Callback Functions

If your function finishes correctly, the callback will execute whether the external contract fails or not.
#[near]
impl Contract {
    #[private]
    pub fn callback_handler(
        &mut self,
        #[callback_result] result: Result<String, PromiseError>
    ) -> String {
        match result {
            Ok(value) => {
                near_sdk::log!("External call succeeded: {}", value);
                // Continue with success logic
                value
            },
            Err(err) => {
                near_sdk::log!("External call failed");
                // Rollback any state changes made in the original call
                // Refund user if needed
                "Failed".to_string()
            },
        }
    }
}
Callback Always ExecutesIf your function finishes correctly, your callback will always execute, even if the external function fails. Always check the result and manually handle failures.

What Happens if External Function Fails?

If the external function panics, your callback still executes. You must:
  1. Refund the predecessor if NEAR was attached (funds are now in the contract’s account)
  2. Revert state changes manually - they won’t rollback automatically
#[private]
pub fn callback_handler(
    &mut self,
    #[callback_result] result: Result<String, PromiseError>
) {
    if result.is_err() {
        // Refund user
        if self.pending_payment > 0 {
            Promise::new(self.user.clone())
                .transfer(NearToken::from_yoctonear(self.pending_payment));
        }
        // Rollback state changes
        self.pending_payment = 0;
        self.order_status = OrderStatus::Cancelled;
    }
}

Batch Calls (Same Contract)

Call multiple functions on the same contract. They act as a unit - if any function fails, they all get reverted.
use near_sdk::{near, Promise, Gas, NearToken};

#[near]
impl Contract {
    pub fn batch_actions(&self) -> Promise {
        let account = "contract.near".parse().unwrap();

        Promise::new(account.clone())
            .function_call(
                "method_one".to_owned(),
                json!({}).to_string().into_bytes(),
                NearToken::from_near(0),
                Gas::from_tgas(5),
            )
            .function_call(
                "method_two".to_owned(),
                json!({}).to_string().into_bytes(),
                NearToken::from_near(0),
                Gas::from_tgas(5),
            )
            .then(
                Self::ext(env::current_account_id())
                    .with_static_gas(Gas::from_tgas(5))
                    .batch_callback()
            )
    }
}
Callbacks only have access to the result of the last function in a batch call.

Parallel Calls (Different Contracts)

Call functions on different contracts. They execute in parallel and don’t impact each other.
#[near]
impl Contract {
    pub fn parallel_calls(&self) -> Promise {
        let promise_a = Promise::new("contract-a.near".parse().unwrap())
            .function_call(
                "method_a".to_owned(),
                vec![],
                NearToken::from_near(0),
                Gas::from_tgas(5),
            );

        let promise_b = Promise::new("contract-b.near".parse().unwrap())
            .function_call(
                "method_b".to_owned(),
                vec![],
                NearToken::from_near(0),
                Gas::from_tgas(5),
            );

        promise_a.and(promise_b).then(
            Self::ext(env::current_account_id())
                .with_static_gas(Gas::from_tgas(5))
                .multi_callback()
        )
    }

    #[private]
    pub fn multi_callback(
        &self,
        #[callback_result] result_a: Result<String, PromiseError>,
        #[callback_result] result_b: Result<String, PromiseError>,
    ) -> Vec<String> {
        let mut results = vec![];
        if let Ok(val) = result_a {
            results.push(val);
        }
        if let Ok(val) = result_b {
            results.push(val);
        }
        results
    }
}
Callbacks have access to the result of all functions in parallel calls.

Security Concerns

Cross-contract calls are independent and asynchronous:
Critical Security Rules
  1. Don’t leave the contract in an exploitable state between call and callback
  2. Manually rollback state changes in the callback if the external call failed
  3. Always mark callbacks as private
  4. Ensure sufficient gas for callback execution
Not following these guidelines could expose your contract to exploits.

Security Best Practices

Learn more about cross-contract call security

Next Steps

Testing

Test cross-contract interactions

Security

Security best practices

Deploy

Deploy your contract

Examples

View example projects

Build docs developers (and LLMs) love