# Resonate Basic Debugging Java

> Debug and troubleshoot Resonate applications using the Java SDK. Use when investigating stuck or never-resuming workflows, duplicated side effects after replay, untyped-handle decode surprises (Integer vs Long), CLI positional-argument arity mismatches, the detached by-name-only constraint, Java 21 / virtual-thread requirements, rejected-promise error handling, or r.stop() silently killing a live worker. Verified against the resonatehq-examples/*-java repos and develop/java.mdx (docs PR

- Skill: `resonatehq/resonate-basic-debugging-java` (Agent Skill)
- Install (CLI): `npx skillmds@latest add resonatehq/resonate-basic-debugging-java`
- Raw SKILL.md: https://api.skillmd.com/api/skills/resonatehq/resonate-basic-debugging-java/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-java

---


# Resonate Basic Debugging — Java

> **Prerelease note.** `resonate-sdk-java` is published on Maven Central — pin `io.resonatehq:resonate-sdk-java:0.1.1`. The API mirrors the Python SDK and may change before a stable `1.0`. **Requires Java 21+** — the SDK uses virtual threads, a feature generally available in Java 21. Every code block here is compile-verified against `0.1.1` with a Java 21 toolchain, and cross-checked against the `resonatehq-examples/*-java` repos and `develop/java.mdx` (docs PR #230).

## Overview

Java's type system catches many bugs at compile time — typed method references and a typed `ResonateHandle` close off whole classes of error the dynamically-typed SDKs hit. The traps that remain are mostly at the durability boundary: untyped-handle decoding, CLI argument binding, replay double-fires, and the by-name-only constraints. This skill is a symptom-first guide to those failure modes.

For the language-agnostic replay and recovery mental model, read `durable-execution` first.

## Triage flow

1. Does it even build / start? Confirm a **Java 21+** toolchain — the SDK uses virtual threads and will not run on Java 8/11/17.
2. Is the worker connected? Confirm the builder's `url(...)` (or `RESONATE_URL`) points at a running server, and `r` was built without throwing.
3. Is the function registered? By-name dispatch (`rpc`, the CLI) needs `r.register(Owner::fn)` on the executing group.
4. Is the promise stuck? Run `resonate promise get <id>` to check state (`pending` / `resolved` / `rejected` / `timedout`).
5. Is the workflow replaying but producing duplicates? An un-checkpointed side effect is re-running above a durable boundary.
6. Is a CLI-invoked function arity-mismatching? Each `--arg` binds to one positional parameter.
7. Is the worker up but not picking up work? `r.stop()` may have been called on a live worker.

---

## Stuck / never-resuming workflows

### Latent promise never settled

**Symptom:** `future.await()` blocks indefinitely; `resonate promise get <id>` shows state `pending`.

**Causes:**
- The external actor never settled the promise. Resolve it via the CLI (`resonate promise resolve <id> --data '"approved"'`) or `r.promises.resolve(id, new Value(null, "approved"))`.
- The decoded type does not match what the awaiter expects, so `await` throws a decode error rather than returning.

Unlike the Go SDK (which has no `promises` sub-client and requires manual base64 encoding of the JSON payload), the **Java SDK ships `r.promises`** — resolve directly:

```java
import io.resonatehq.resonate.Types.Value;

r.promises.resolve("approval-1", new Value(null, "approved")).join();
```

See `resonate-human-in-the-loop-pattern-java` for the full mechanics.

### `ctx.run` leaf blocks indefinitely

**Symptom:** the workflow task lease expires and the server reassigns the task; the workflow appears to restart rather than resume; attempts never complete.

**Cause:** the runtime drains every `ctx.run`-spawned future before it can suspend or settle the parent task. A function that does external I/O, waits on a lock, or sleeps for a long time inside `ctx.run` holds the lease open until the TTL expires (default 60s).

**Fix:** move long-running or external-blocking work into `ctx.rpc` (remote dispatch — the workflow suspends cleanly) or `ctx.promise` (latent promise settled by an external actor). Reserve `ctx.run` for in-process computation that returns promptly.

---

## Duplicated side effects (replay)

**Symptom:** emails, charges, log entries, or DB writes happen more than once per logical invocation.

**Cause:** the entire workflow body re-runs from the top on every resume. Durable child promises short-circuit work that already settled, but code that runs *before* reaching a durable boundary (`ctx.sleep`, `ctx.rpc`, `ctx.promise`) executes again on each replay pass.

```java
// BAD — the log line re-executes on every replay pass.
public static String myWorkflow(Context ctx, String id) {
    System.out.println("charging card for order " + id); // runs on every replay
    return ctx.run(MyWorkflow::chargeCard, id).await();
}

// GOOD — the side effect is inside a checkpointed ctx.run; it runs once.
public static String myWorkflow(Context ctx, String id) {
    return ctx.run(MyWorkflow::chargeCard, id).await(); // result is checkpointed
}
```

**Rule:** any observable side effect (network call, write, notification) belongs inside its own `ctx.run` or `ctx.rpc` so the durable promise records the result and short-circuits on replay.

---

## Decode and error handling

### Untyped handle: `Integer` vs `Long`

**Symptom:** `ClassCastException` reading a numeric result from an untyped handle, e.g. `(Long) handle.result()` throws on a small number.

**Cause:** the by-name `r.run` / `r.rpc` forms and `r.get` return `ResonateHandle<Object>`. Jackson decodes a small JSON integer as `Integer` and a large one as `Long`. A direct cast to one or the other is fragile.

**Fix:** read through `Number` (this is exactly what `example-recursive-factorial-java` does, because `13!` overflows `int`):

```java
long result = ((Number) handle.result()).longValue();
```

A method-reference invocation returns a typed `ResonateHandle<R>` and avoids the issue entirely — prefer it where the function is registered locally.

### Generic collection result from a by-name invocation

**Symptom:** a function that returns `List<MyRecord>` (or `Map<String, MyRecord>`) comes back as `List<LinkedHashMap>` when read from a by-name call — a `ClassCastException` or surprising field access on the elements.

**Cause:** the by-name forms (`ctx.rpc("name", ...)`, `r.rpc(id, "name", ...)`, `r.get(id)`) decode against `Object` (`Resonate.java`), so Jackson reconstructs only generic JSON shapes — `LinkedHashMap` per element — and the record type parameter is lost at the boundary.

**Fix:** use the **method-reference** form, which decodes against the function's full generic return type and gives a typed future:

```java
// Typed: ctx.run / ctx.rpc with a method reference decodes List<Delivery> correctly.
ResonateFuture<List<Delivery>> f = ctx.run(Orders::deliverAll, batch);
List<Delivery> delivered = f.await();
```

If you must read it from an untyped by-name result, reshape it with a Jackson `ObjectMapper.convertValue(result, new TypeReference<List<Delivery>>() {})` — note that pulls Jackson onto your *compile* classpath (the SDK only depends on it transitively at runtime), so add `com.fasterxml.jackson.core:jackson-databind` to your build to do this.

### CLI invocation arity mismatch

**Symptom:** invoking from the CLI fails to bind arguments, or the function receives the wrong values / an arity error.

**Cause:** `resonate invoke <id> --func f --arg 5 --arg 1` passes the args as a positional list, and the Java SDK binds them **one per declared parameter**. A function declaring a single `int[]` (or `List`) parameter arity-mismatches two `--arg` values.

**Fix:** declare one parameter per `--arg`:

```java
// resonate invoke countdown.1 --func countdown --arg 5 --arg 1  →  count=5, delaySeconds=1
public static String countdown(Context ctx, int count, int delaySeconds) { ... }
```

`example-quickstart-java` is the canonical reference. (This is the opposite of Go, where the same invoke binds the whole list to a single `[]int`.)

### Instance method reference registered as a durable function

**Symptom:** an `Owner::fn` reference compiles, but execution fails or behaves unexpectedly when the referenced method is an instance method (`this::fn`, `someObject::fn`).

**Cause:** durable functions must be `public static`. The SDK recovers the method behind a reference by reflection and invokes it without an object instance, so an instance method — which needs a receiver — has nowhere to run from on the worker.

**Fix:** make every registered function and every `ctx.run` / `ctx.rpc` target a `public static` method. Hold any per-instance state as a dependency (`r.withDependency` / `ctx.getDependency`) instead of closing over `this`.

### Rejected promise re-throws the application error

**Symptom:** `handle.result()` or `future.await()` throws even though no local exception was raised at the call site.

**Cause:** the promise was rejected — either by the registered function throwing, or by an external `resonate promise reject <id>`. The error is re-thrown by its real type where reconstructable across the durability boundary, otherwise as an `ApplicationError` carrying the message.

```java
import io.resonatehq.resonate.Errors.ApplicationError;

try {
    String result = handle.result();
} catch (ApplicationError ae) {
    System.err.println("workflow rejected: " + ae.getMessage());
}
```

### `ctx.detached` with a method reference does not compile

**Symptom:** `ctx.detached(Owner::fn, args)` fails to compile.

**Cause:** `ctx.detached` is **by-name `String` only** — there is no method-reference overload (verified `Context.java:600`), unlike `ctx.run` / `ctx.rpc`.

**Fix:** pass the registered name as a `String`, and ensure the target is registered on whichever group executes it:

```java
String auditId = ctx.detached("writeAuditLog", orderId).id();
```

---

## Setup footguns

### Wrong Java version

**Symptom:** `UnsupportedClassVersionError`, or virtual-thread APIs missing at runtime.

**Cause:** the SDK requires Java 21 (virtual threads). Java 8/11/17 will not work.

**Fix:** set the toolchain to 21:

```kotlin
java { toolchain { languageVersion = JavaLanguageVersion.of(21) } }
```

### `r.stop()` on a long-running worker

**Symptom:** the worker process is running and healthy-looking, but stops picking up new tasks.

**Cause:** `r.stop()` closes the server connection, stops the heartbeat loop, and cancels the subscription-refresh loop. In-flight leased tasks have their TTL expire and the server reassigns them. The process keeps running, but the dispatch pipeline is dead.

**Rule:** call `r.stop()` only in one-shot binaries, demos, and CI tasks that exit after their work finishes. A long-running worker should stay up — block on a latch and let SIGINT / SIGTERM end the process:

```java
import java.util.concurrent.CountDownLatch;

// Correct for a worker: register, then block — never stop.
r.register(Quickstart::countdown);
new CountDownLatch(1).await();
```

---

## Inspection tools

```shell
resonate dev                                      # local dev server
resonate promise get <id>                         # single promise state + value
resonate promise search 'order:*'                 # prefix search across promises
resonate promise resolve <id> --data '"approved"' # settle a pending latent promise
resonate tree <id>                                # call graph for an invocation
```

See the `resonate-cli` skill for the full command surface. The CLI is SDK-agnostic; the same commands work against any worker language.

**Durable sleep tolerance:** a 24h `ctx.sleep` firing in 23–25h is within the server's timer tolerance window, not a bug.

---

## Avoid

- Branching on `System.currentTimeMillis()`, `Math.random()`, or `UUID.randomUUID()` directly inside a workflow body — non-deterministic values change between replay passes and cause divergent execution. Move them into a leaf so the result is checkpointed.
- Casting a numeric result from an untyped `ResonateHandle<Object>` straight to `Long` (or `Integer`) — read through `Number`.
- Declaring a single array/`List` parameter for a CLI-invoked function — `--arg` values bind positionally, one per parameter.
- Passing more than five application arguments to a durable function — `Fn.F0`–`Fn.F5` caps it at five beyond the `Context`. Bundle extras into a record.
- Passing non-serializable types (open streams, thread handles) as workflow args — they encode into the durable promise via JSON.

---

## Related skills

- `resonate-basic-durable-world-usage-java` — Context APIs (`ctx.run`, `ctx.rpc`, `ctx.sleep`, `ctx.promise`, `ctx.detached`), the replay model
- `resonate-basic-ephemeral-world-usage-java` — the builder, `register`, typed vs untyped handles, `stop` semantics
- `resonate-human-in-the-loop-pattern-java` — latent-promise resolution via `r.promises.resolve`
- `resonate-cli` — full CLI command surface for promise inspection and settlement
- `resonate-defaults` — default TTL, retry policy, and timeout values across all SDKs
- `durable-execution` — foundational replay and recovery model
- `resonate-basic-debugging-python` — the closest sibling; the Java API mirrors Python

