- name
- resonate-basic-debugging-rust
- description
- Debug and troubleshoot Resonate applications using the Rust SDK. Use when investigating registration errors, serde serialization failures, tokio runtime mismatches, install issues, or SDK-version-specific caveats of the early-development Rust SDK.
- license
- Apache-2.0
# Resonate Basic Debugging — Rust
> **SDK note.** This SDK is in active development (v0.6.0, published on crates.io), and documented behaviors may shift between releases. Failure modes listed here are what the documented surface produces; new ones will appear as the SDK grows.
## Overview
Rust's failure modes differ from TS's and Python's. Compile-time type errors catch many bugs the other SDKs only discover at runtime; the remaining runtime failures are usually serde, tokio, or registration-name related. This skill covers the shapes you'll actually see.
For the language-agnostic replay + recovery mental model, read `durable-execution` first.
## Triage flow
1. **Does it compile?** If `cargo build` fails, you're in type-error territory (likely: missing `Result<T>` return, wrong first-parameter type, forgotten `&` on Context/Info, missing `?` on an await)
2. **Does it register?** `resonate.register(fn)` returns a `Result`; unwrap it and look at the error (duplicate name, bad signature)
3. **Does the function kind match expectations?** The SDK infers kind from the first parameter — `&Context` vs `&Info` vs a value type
4. **Is serde happy?** Input/output types need `Serialize` + `Deserialize` derives; stack traces mentioning `serde_json::Error` mean a type doesn't derive correctly
5. **Is the tokio runtime correct?** `#[tokio::main]` or `#[tokio::test(flavor = "multi_thread")]` needed; single-threaded runtime can deadlock on SDK internals
6. **Check server + SDK compatibility.** The SDK version in `Cargo.toml` must align with the server version you're testing against; check the SDK CHANGELOG for compatibility notes
## Install / dependency issues
**Symptom:** `cannot find crate 'resonate'` or version-resolution errors.
**Cause:** Check your `Cargo.toml`. The SDK is published on crates.io — pin the current release:
```toml
[dependencies]
resonate = { package = "resonate-sdk", version = "0.6" }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
```
For pre-release work that must track `main` directly (e.g. to pick up a not-yet-released fix):
```toml
resonate = { package = "resonate-sdk", git = "https://github.com/resonatehq/resonate-sdk-rs", branch = "main" }
```
Run `cargo update -p resonate-sdk` to pull the latest version when upstream changes.
## Registration errors
| Symptom | Likely cause | Fix |
|---|---|---|
| `register` returns `Err(AlreadyRegistered)` | Same function registered twice | Register once per process; check for accidentally-looped registration in tests |
| `register` returns `Err(BadSignature)` | Function doesn't match one of the 3 valid shapes | First param must be `&Context`, `&Info`, or a value type deriving `Deserialize`. Return must be `Result<T>` where T derives `Serialize` |
| Runtime `FunctionNotRegistered` on an RPC call | Caller's name doesn't match registered name | If you used `#[resonate::function(name = "custom")]`, callers must use `"custom"`. Default registered name is the function's Rust identifier |
## Serde errors
**Symptom:** `serde_json::Error { ... }` at runtime when a function result crosses a checkpoint or RPC boundary.
**Cause:** Input or output type doesn't derive `Serialize` / `Deserialize`. Every argument and return value gets serialized by Resonate.
**Fix:**
```rust
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Order {
id: String,
amount: f64,
}
#[resonate::function]
async fn process(ctx: &Context, order: Order) -> Result<Order> {
// ...
Ok(order)
}
```
Common sub-issues:
- `#[serde(rename_all = "snake_case")]` if your JSON payload conventions differ from Rust field names
- `#[serde(skip_serializing_if = "Option::is_none")]` for optional fields
- Enums need `#[serde(tag = "type")]` for internally-tagged representations; pick a representation and stick with it across all workers
## `ctx` vs `info` confusion
**Symptom:** "Cannot find method `run` on `&Info`" at compile time.
**Cause:** Only `&Context` has `run`, `rpc`, `sleep`. `&Info` gives metadata but not execution capabilities.
**Fix:** use `&Context` as the first parameter when the function needs to orchestrate sub-tasks.
| You want | Use |
|---|---|
| Sub-task invocation | `ctx: &Context` |
| Metadata only (read execution ID, parent ID) | `info: &Info` |
| Stateless pure computation | no Context/Info; just value types |
## Missing `?` on `.await`
**Symptom:** compile error about `Result<String>` doesn't implement something, or `future cannot be awaited`.
**Cause:** Every async method in the SDK returns a `Future<Output = Result<T>>`. Awaiting gives you `Result<T>`; you still need `?` to extract `T`:
```rust
// Bad — `result` is Result<String>, not String
let result = ctx.run(leaf, "input".into()).await;
// Good
let result: String = ctx.run(leaf, "input".into()).await?;
```
## `.spawn()` is synchronous — don't add `.await` after it
**Symptom:** compile error like "no method named `poll` found", a type-mismatch where the compiler expected `DurableFuture<T>` but got an opaque future type, or a lifetime/borrow error that only appears on the `.spawn()` line.
**Cause:** As of SDK 0.5.0+, `ctx.run(...).spawn()` / `ctx.rpc(...).spawn()` on a `Context` is **synchronous** — it returns the handle directly (`ctx.run` → `Result<DurableFuture<T>>`, `ctx.rpc` → `Result<RemoteFuture<T>>`), without suspending. Adding `.await` after `.spawn()` is an old habit that no longer compiles.
```rust
// BAD — .await after .spawn() fails to compile in 0.5.0+
let fut = ctx.run(leaf, "input".into()).spawn().await?;
// CORRECT — .spawn() is sync; unwrap with ? then await the DurableFuture later
let fut = ctx.run(leaf, "input".into()).spawn()?;
// ... do other work ...
// later: a single .await on the DurableFuture gets the actual T
let result: String = fut.await?;
```
This differs from TS/Python's `begin_run` / `ctx.rfi`, which return a promise-like handle from an async call. In Rust, you use `?` (not `.await?`) to unwrap the `Result<DurableFuture>` from `.spawn()`, then a single `.await?` later to get `T`.
## tokio runtime mismatches
**Symptom:** deadlock at startup, or "Cannot drop runtime in a runtime" panic.
**Cause:** wrong tokio runtime flavor, or nested `Runtime::block_on` inside an already-running runtime.
**Fix:** use `#[tokio::main]` on your entry point with the default multi-threaded runtime. Do NOT wrap Resonate calls in `Handle::block_on` inside a durable function.
```rust
#[tokio::main]
async fn main() -> Result<()> {
let resonate = Resonate::local();
resonate.register(my_fn).unwrap();
let result: String = resonate.run("id", my_fn, "input".into()).await?;
println!("{}", result);
resonate.stop().await?;
Ok(())
}
```
For tests, prefer `#[tokio::test(flavor = "multi_thread")]` over the default single-thread flavor when tests involve multiple workers.
## Non-determinism regressions
Durable functions replay from the last checkpoint. Any non-deterministic code above a checkpoint can cause divergence.
Common Rust-specific footguns:
```rust
// BAD — system time changes between runs
#[resonate::function]
async fn bad(ctx: &Context) -> Result<()> {
let now = std::time::SystemTime::now();
if now > SOME_THRESHOLD {
ctx.run(branch_a, "".into()).await?;
} else {
ctx.run(branch_b, "".into()).await?;
}
Ok(())
}
// BAD — random values change between runs
use rand::Rng;
#[resonate::function]
async fn bad2(ctx: &Context) -> Result<()> {
let roll = rand::thread_rng().gen_range(0..100);
// branch on roll — different on each replay
Ok(())
}
```
The SDK does NOT expose `ctx.time.time()` / `ctx.random.random()` helpers. Until those land, the safe pattern is:
- Do non-deterministic work inside a leaf (so the value is checkpointed)
- Or derive branches from the invocation's stable ID / input args, not runtime randomness
```rust
#[resonate::function]
async fn good(ctx: &Context, input: String) -> Result<()> {
// random work inside a checkpointed leaf
let roll = ctx.run(roll_dice, ()).await?;
if roll > 50 {
ctx.run(branch_a, input).await?;
} else {
ctx.run(branch_b, input).await?;
}
Ok(())
}
#[resonate::function]
async fn roll_dice(_: ()) -> Result<u32> {
Ok(rand::thread_rng().gen_range(0..100))
}
```
## Minimal repro
```rust
// src/main.rs
use resonate::prelude::*;
#[tokio::main]
async fn main() -> Result<()> {
let resonate = Resonate::local();
resonate.register(ping).unwrap();
let result: String = resonate.run("ping:alice", ping, "Alice".into()).await?;
println!("{}", result);
resonate.stop().await?;
Ok(())
}
#[resonate::function]
async fn ping(name: String) -> Result<String> {
Ok(format!("pong {}", name))
}
```
If this fails, the problem is infrastructure (Cargo deps, tokio runtime, SDK version). If it succeeds but your real code fails, diff your function signatures against this template.
## Server compatibility
The Rust SDK (v0.6.0) ships with tested compatibility against the Resonate server; check the SDK CHANGELOG for the minimum server version. Before reporting a bug, verify:
1. Your SDK version — `cargo metadata | grep resonate` for the resolved version
2. Your server version — `resonate --version` on the server binary
3. The SDK's compatibility notes — check the SDK's CHANGELOG or README on GitHub
Expect that the API surface may shift between 0.x releases until the SDK reaches 1.0.
## CLI one-liners
```shell
resonate dev # local dev server
resonate tree <invocation-id> # call-graph
resonate promises get <id> # single promise state
resonate promises search 'order:*' # prefix search
resonate promises resolve <id> --data '{}' # settle a pending promise
```
The CLI is SDK-agnostic; same commands work for TS, Python, Rust worker ecosystems.
## Rust SDK API coverage status
Cross-reference with `resonate-basic-durable-world-usage-rust` for the full treatment; quick summary for debug triage:
### Exists in v0.6.0 source (even if `rust.mdx` doesn't mention it)
- `ctx.promise::<T>()` — Context-side HITL primitive (source: `resonate/src/context.rs:335`)
- `ctx.get_dependency::<T>()` + `Info::get_dependency::<T>()` — type-dispatched DI (source: `context.rs:120`, `info.rs:43`)
- `ctx.detached(func, args)` — fire-and-forget remote execution (source: `context.rs:405`); `.spawn()?` returns a `DetachedHandle`, and `handle.id().await?` gives the durable promise ID
- `ctx.info()` returning extra accessors `branch_id`, `tags`
- `resonate.with_dependency::<T>(value)` — ephemeral-side DI builder
If a workflow is mysteriously missing one of these, the issue is likely *docs staleness*, not SDK absence. Cite source paths when an agent reviewer questions whether an API exists.
### NOT in v0.6.0 source
- `ctx.random.random()` / `ctx.time.time()` — do non-det work inside a leaf so it's checkpointed
- `ctx.panic()` / `ctx.assert()` — use Rust's `panic!` / `assert!` (non-recoverable) or `Result` propagation (recoverable)
Each of these may land in a future version; check `docs/develop/rust.mdx` AND the `resonate-sdk-rs` source when a new release ships — iter-18/19 review showed docs can lag source meaningfully.
## Related skills
- `resonate-basic-ephemeral-world-usage-rust` — Client APIs at the process-entry layer
- `resonate-basic-durable-world-usage-rust` — Context APIs inside durable functions
- `durable-execution` + `resonate-philosophy` — foundational; many debug sessions end up being about patterns warned against here
- `resonate-basic-debugging-typescript` + `-python` — sibling SDKs for comparison
在 GitHub 查看