SoulFire LogoSoulFire

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

FailureMeaningTypical response
Connection or compatibilityHandshake, API version, or required capability failedStop startup and report the incompatibility
RPCThe remote call failed before producing a semantic resultInspect code and retryable
ActionThe server processed an atomic action but did not complete itInspect the action result and correct state or input
TaskA durable task reached a failed or cancelled terminal stateInspect failure code, progress, and retryability
PluginDiscovery, descriptor, compatibility, or plugin invocation failedCheck installation, version, and grants
Local lifecycleA session, lease, container, or client is already closedReopen 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:

retry-read.ts
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

On this page