Errors, retries, and cancellation
Handle SoulFire errors without losing information that helps you find the cause.
SoulFire separates transport errors, rejected game actions, and failed tasks. The error type tells the application whether to retry, change input, inspect progress, or request permission.
Error families
| Failure | Meaning | Typical response |
|---|---|---|
| Connection or compatibility | Handshake, API version, or required capability failed | Stop startup and report the incompatibility |
| RPC | The remote call failed before producing a semantic result | Inspect code and retryable |
| Action | The server processed an atomic action but did not complete it | Inspect the action result and correct state or input |
| Task | A durable task reached a failed or cancelled terminal state | Inspect failure code, progress, and retryability |
| Plugin | Discovery, descriptor, compatibility, or plugin invocation failed | Check installation, version, and grants |
| Local lifecycle | A session, lease, container, or client is already closed | Reopen the scoped resource |
Effect error channels
The Effect v4 API exposes tagged errors. Use Effect.catchTag to recover from one error family.
For a read-only RPC, restrict retries to retryable transport failures:
import { Effect, Schedule } from "effect";
import { SoulFire } from "@soulfiremc/sdk";
const program = Effect.scoped(Effect.gen(function* () {
const client = yield* SoulFire.connect({
baseUrl: "https://soulfire.example.com",
token: process.env.SOULFIRE_TOKEN,
});
const bot = client.instance("instance-uuid").bot("bot-uuid");
const info = yield* bot.info().pipe(
Effect.retry({
schedule: Schedule.exponential("100 millis"),
times: 4,
while: (error) => error._tag === "SoulFireRpcError" && error.retryable,
}),
);
yield* Effect.logInfo(info.status);
}));
await Effect.runPromise(program);SoulFireRpcError preserves the operation name, ConnectRPC code, request ID,
retryability, and original cause. Connection, task, plugin, and behavior
operations use their own tagged error types.
This example retries a read at most four times after its first attempt. It does not repeat a chat mutation.
Effect interruption cancels the underlying request or stream. Acquire clients, sessions, leases, and local server processes in a scope so interruption also runs their finalizers.
Python error channels
Python effects preserve typed failures separately from unexpected exceptions.
Use effect-py recovery operators instead of try around yield from:
from effect_py import fail, succeed
from effect_py.errors import catch_tag
from soulfire import SoulFireRpcError
operation = bot.chat.send("hello").pipe(
catch_tag(SoulFireRpcError)(
lambda error: succeed(None) if error.retryable else fail(error)
)
)This example accepts a retryable delivery failure without sending a duplicate message. Use a bounded retry policy for operations that can safely repeat. Unexpected exceptions remain defects and bypass typed recovery.
Use run_async_exit to inspect a complete workflow's result.
Use or_die() only at the application boundary after recovery ends.
Register cleanup in the scope. Do not yield cleanup effects from a generator's finally block.
Cancellation and deadlines
In TypeScript, pass an AbortSignal to Effect.runPromise at the host boundary.
Interruption propagates to scoped fibers, requests, and streams.
In Python, cancel the async task that runs the complete effect workflow.
The runtime waits for scoped finalizers before cancellation completes.
Python effect durations use seconds. RPC timeout_ms options retain milliseconds.
Retry safely
Retry only when both conditions hold:
- The error reports
retryable. - Repeating the operation cannot duplicate a game-side mutation.
Use an idempotency key for supported mutations and task starts. SoulFire stores the completed response for the key within its bounded retention window. A retry with the same key receives the original result instead of repeating the action.
Do not retry invalid arguments, denied permissions, stale entity epochs, closed containers, unreachable path goals, or deterministic plugin validation errors without changing the request.
A transport timeout does not prove that the server did nothing. Use an idempotency key or query the durable task handle before submitting the same mutation again.
Include the request ID, operation, server version, SDK version, bot ID, and task ID in production logs. Do not log access tokens, plugin secrets, chat credentials, or raw packet payloads that can contain private data.
How is this page?
Last updated on
