Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
336 changes: 319 additions & 17 deletions docs/guides/canister-calls/onchain-calls.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,322 @@ sidebar:
icskills: [multi-canister]
---

TODO: Write content for this page.

<!-- Content Brief -->
Call functions on other canisters from your canister code. Start with the query vs update decision: queries are fast (~200ms), free, and run on a single node but aren't replicated (caller must trust the node); updates go through consensus (~2s), cost cycles, and modify state reliably. Explain when to use each and the trust tradeoffs. Cover canister discovery (env vars, ic_env cookie), error handling (reject codes, trap handling), calling third-party canisters, and the pub/sub pattern. Show Rust (ic_cdk::call) and Motoko (async message sends) patterns for both query and update calls. Explain the 2MB response limit. Link to parallel-calls for composite queries (the advanced query pattern) and to certified-variables for making query responses verifiable.

<!-- Source Material -->
- Portal: building-apps/integrations/advanced-calls.mdx, advanced/using-third-party-canisters.mdx, developer-tools/cdks/rust/intercanister.mdx
- icp-cli: concepts/canister-discovery.md
- icskills: multi-canister
- Examples: inter-canister-calls (Rust), pub-sub (Motoko), composite_query (both)

<!-- Cross-Links -->
- concepts/canisters -- messaging model
- guides/canister-calls/candid -- interface definitions
- guides/canister-calls/parallel-calls -- concurrent calls and composite queries
- guides/backends/certified-variables -- making query responses verifiable
- guides/security/inter-canister-calls -- async safety
Canisters on the Internet Computer communicate by calling each other's functions. A caller canister sends a request message containing the method name, arguments, and optionally attached cycles. The callee executes the method and returns a response. If the callee cannot be reached or a resource limit is hit, the system produces a reject response instead.

This guide covers making inter-canister calls in both Motoko and Rust, choosing between query and update calls, handling errors, and avoiding common pitfalls. For the messaging model behind these calls, see [Canisters](../../concepts/canisters.md).

## Query vs update calls

There are two types of canister methods, and the choice affects latency, cost, and trust:

| | Query | Update |
|---|---|---|
| **Latency** | ~200ms | ~2s |
| **Cycle cost** | Free | Costs cycles |
| **Execution** | Single replica | Full consensus |
| **State changes** | Not persisted | Persisted |
| **Trust model** | Caller trusts one replica | Replicated and verifiable |

**Use query calls** when reading data where the caller trusts the subnet (or will verify the response independently). Queries are fast and free, but the response comes from a single node and is not replicated.

**Use update calls** when modifying state, transferring cycles, or when the caller needs a consensus-backed guarantee that the call was executed correctly.

To make query responses verifiable without the cost of update calls, see [Certified Variables](../backends/certified-variables.md). For running multiple query calls in parallel, see [Parallel Calls and Composite Queries](parallel-calls.md).

## Making calls in Motoko

Import another canister by name and call its methods with `await`:

> **Note:** The `canister:name` import syntax is being redesigned for icp-cli compatibility. See [Canister discovery](#canister-discovery) for the recommended environment variable approach.

```motoko
import Counter "canister:counter";

persistent actor {
public func getCount() : async Nat {
await Counter.get()
};

public func incrementAndGet() : async Nat {
await Counter.increment();
await Counter.get()
};
};
```

### Error handling in Motoko

Wrap inter-canister calls in `try`/`catch` to handle rejects:

```motoko
import Counter "canister:counter";
import Error "mo:core/Error";
import Result "mo:core/Result";

persistent actor {
public shared ({ caller }) func safeIncrement() : async Result.Result<Nat, Text> {
try {
await Counter.increment();
let count = await Counter.get();
#ok(count)
} catch (e) {
#err("Counter call failed: " # Error.message(e))
};
};
};
```

In Motoko, `public shared ({ caller })` binds the original caller at method entry, so `caller` remains valid after `await` points.

## Making calls in Rust

The Rust CDK provides `Call::unbounded_wait` and `Call::bounded_wait` to call other canisters. Both return a builder that lets you attach arguments, cycles, and timeout settings.

### Basic call

```rust
use candid::{Nat, Principal};
use ic_cdk::call::Call;
use ic_cdk::update;

#[update]
pub async fn call_get_and_set(counter: Principal, new_value: Nat) -> Nat {
Call::unbounded_wait(counter, "get_and_set")
.with_arg(&new_value)
.await
.expect("Failed to get the old value")
.candid::<Nat>()
.expect("Candid decoding failed")
}
```

### Error handling in Rust

The call returns a `Result` where the error type distinguishes **clean rejects** (the call definitively did not execute) from **non-clean rejects** (the outcome is unknown):

```rust
use candid::Principal;
use ic_cdk::call::{Call, CallErrorExt};
use ic_cdk::update;

#[update]
pub async fn call_increment(counter: Principal) -> Result<(), String> {
match Call::unbounded_wait(counter, "increment").await {
Ok(_) => Ok(()),
Err(e) if !e.is_clean_reject() => {
Err(format!("Non-clean reject: {:?}. Outcome unknown.", e))
}
Err(e) => {
Err(format!("Clean reject: {:?}. Counter was not incremented.", e))
}
}
}
```

The distinction matters for correctness:

- **Clean reject** -- the callee never executed the method. Safe to retry.
- **Non-clean reject** -- the callee may or may not have executed. Use idempotent APIs or provide a separate endpoint to query the outcome.

## Canister discovery

Before making an inter-canister call, your canister needs the `Principal` of the target canister. Canister IDs are assigned at deployment time and differ between environments (local, staging, mainnet), so hardcoding them creates portability problems.

### Environment variables (recommended)

`icp deploy` automatically injects `PUBLIC_CANISTER_ID:<name>` environment variables into every canister in the environment. This means each canister can discover any other canister's ID at runtime without hardcoding:

**Rust:**

```rust
use candid::Principal;

let counter_id = Principal::from_text(
&ic_cdk::api::env_var_value("PUBLIC_CANISTER_ID:counter")
).unwrap();
```

**Motoko:**

```motoko
import Runtime "mo:core/Runtime";
import Principal "mo:core/Principal";

let ?counterIdText = Runtime.envVar<system>("PUBLIC_CANISTER_ID:counter") else {
return #err("counter canister ID not set");
};
let counterId = Principal.fromText(counterIdText);
```

Deployment order does not matter — `icp deploy` creates all canisters first, then injects variables, then installs code. Variables are only updated for the canisters being deployed, so run `icp deploy` (without arguments) when adding new canisters to update all of them.

### Alternative approaches

**Init arguments.** Accept the target `Principal` as an `#[init]` argument and store it. This avoids the environment variable lookup at call time but requires passing the ID at every deploy and upgrade:

```bash
TARGET_ID=$(icp canister id counter)
icp deploy my_canister --argument "(principal \"$TARGET_ID\")"
```

**Hardcoded principal.** Acceptable for well-known system canisters (like the management canister `aaaaa-aa` or the NNS ledger). Avoid for application canisters.

> **Motoko named imports:** Motoko's `import Counter "canister:counter"` syntax resolves canister IDs at compile time. This syntax is currently being redesigned to work with icp-cli's environment-based discovery model. Use environment variables for now if you are building with icp-cli.
<!-- Needs human verification: check back with Motoko team on the status of canister:name redesign -->

## Bounded vs unbounded wait

Every inter-canister call must choose a wait strategy:

### Unbounded wait

```rust
Call::unbounded_wait(callee, "method")
```

The caller waits until the callee produces a response. Response delivery (including rejects) is guaranteed. Use this for calls to canisters you control, where you trust the callee to respond in a timely manner.

### Bounded wait

```rust
use ic_cdk::call::Call;

Call::bounded_wait(callee, "method")
.change_timeout(5) // timeout in seconds
.await
```

The caller may receive a `SYS_UNKNOWN` response after the timeout expires or if the subnet runs low on resources. Response delivery is best-effort. Use this for calls to third-party or untrusted canisters.

**Upgrade safety:** unbounded wait calls may prevent your canister from upgrading until the callee responds. If the callee is unresponsive or malicious, your canister could be stuck indefinitely. Prefer bounded wait when calling canisters you do not control.

**Calling third-party canisters:** When calling canisters outside your control, always use bounded wait and design for uncertainty. The callee may be upgraded, become unresponsive, or behave unexpectedly. Use idempotent operations where possible and provide a way to query the outcome of a call separately, so your canister can recover from ambiguous responses.

## Pub/sub pattern

The publisher/subscriber pattern is a natural fit for inter-canister communication on ICP. A publisher canister maintains a list of subscribers and notifies them when events occur. Unlike traditional pub/sub systems, ICP's reliable message delivery means subscribers are guaranteed to receive notifications (as long as both canisters have sufficient cycles).

### Publisher

The publisher stores subscriber callbacks and invokes them when publishing:

```motoko
import List "mo:core/List";

persistent actor Publisher {

type Event = { topic : Text; value : Nat };

type Subscriber = {
topic : Text;
callback : shared Event -> ();
};

var subscribers = List.empty<Subscriber>();

public func subscribe(subscriber : Subscriber) {
List.add(subscribers, subscriber);
};

public func publish(event : Event) {
for (sub in List.values(subscribers)) {
if (sub.topic == event.topic) {
sub.callback(event);
};
};
};
};
```

### Subscriber

The subscriber registers a callback with the publisher using an inter-canister call:

```motoko
import Publisher "canister:pub";

persistent actor Subscriber {

type Event = { topic : Text; value : Nat };

var count : Nat = 0;

public func init(topic : Text) {
Publisher.subscribe({
topic;
callback = onEvent;
});
};

public func onEvent(event : Event) {
count += event.value;
};

public query func getCount() : async Nat { count };
};
```

The key mechanism is passing a **shared function reference** (`callback`) across canisters. When the publisher calls `sub.callback(event)`, it makes an inter-canister call back to the subscriber.

> **Note:** The subscriber uses `canister:pub` to import the publisher. See [Canister discovery](#canister-discovery) for the note on this syntax and the recommended environment variable alternative.

<!-- TODO: Create a Rust pub/sub example in dfinity/examples — Rust currently has no equivalent of this Motoko pattern in the examples repo -->

## Important caveats

### 2 MB payload limit

Request and response payloads are each limited to 2 MB. For larger data transfers, chunk the payload across multiple calls.

### Non-atomic execution across await

Update methods that make inter-canister calls are **not** executed atomically. Code before `await` runs as one atomic message; code after `await` runs as a separate message. If the callback traps after `await`:

- State changes made **before** `await` are persisted
- State changes made **after** `await` are rolled back

This means a trap in your callback does not undo work done before the call. Design accordingly -- use idempotent operations and check postconditions.

### Caller identity across await (Rust)

In Rust, `ic_cdk::api::msg_caller()` returns the caller of the **current message**, not the original ingress caller. After an `await`, the "caller" is the callee returning a response. Always bind the caller to a local variable before the first `await`:

```rust
#[update]
pub async fn transfer(ledger: Principal, to: Principal, amount: Nat) -> Result<(), String> {
let caller = ic_cdk::api::msg_caller(); // Bind BEFORE await

Call::unbounded_wait(ledger, "transfer")
.with_arg(&(caller, to, amount))
.await
.map_err(|e| format!("Transfer failed: {:?}", e))?;

ic_cdk::println!("Transfer initiated by {}", caller); // Safe: captured before await
Ok(())
}
```

In Motoko, `public shared ({ caller })` captures the original caller at method entry, so this issue does not apply.

### Reentrancy

Inter-canister calls are not atomic, which creates reentrancy risks. Between your outgoing call and the callback, other messages (including calls from the same canister) can execute and modify state. This can lead to double-spending or other inconsistencies.

Mitigate with locking patterns: set a flag before the call, clear it in the callback. For detailed guidance, see [Inter-Canister Call Security](../security/inter-canister-calls.md).

### canister_inspect_message does not apply

The `canister_inspect_message` hook is only called for ingress messages (calls from external users). It is **not** called for inter-canister calls. Do not rely on it for access control between canisters -- perform authorization checks inside the method body instead.

### Cross-subnet latency

Calls between canisters on the same subnet complete within a single round. Cross-subnet calls require 2-3 consensus rounds and are noticeably slower. Keep this in mind when designing multi-canister architectures.

## What's next

- [Parallel Calls and Composite Queries](parallel-calls.md) -- make multiple calls concurrently and use composite queries for efficient read patterns
- [Candid](candid.md) -- define the interface your canister exposes for inter-canister calls
- [Certified Variables](../backends/certified-variables.md) -- make query responses verifiable without update call overhead
- [Inter-Canister Call Security](../security/inter-canister-calls.md) -- reentrancy guards, async safety patterns, and trust considerations

<!-- Upstream: informed by dfinity/portal docs/building-apps/interact-with-canisters/advanced-calls.mdx, docs/building-apps/developer-tools/cdks/rust/intercanister.mdx, multi-canister icskill, icp-cli icskill, dfinity/icp-cli docs/concepts/canister-discovery.md, and dfinity/examples motoko/pub-sub -->
Loading