# Resonate Basic Debugging Rust

> 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.

- Skill: `resonatehq/resonate-basic-debugging-rust` (Agent Skill)
- Install (CLI): `npx skillmds@latest add resonatehq/resonate-basic-debugging-rust`
- Raw SKILL.md: https://api.skillmd.com/api/skills/resonatehq/resonate-basic-debugging-rust/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: Apache-2.0
- Author: resonatehq (https://skillmd.com/u/resonatehq)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/resonatehq/resonate-basic-debugging-rust

---


# 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

