When an invocation fails, the runtime returns an ErrorDetail payload inside the response envelope. Each adapter maps this to a native exception or error type.

ErrorDetail

Every failed response carries a machine-readable code, a human-readable message, and optional structured details:

{
  "id": "<uuid>",
  "ok": false,
  "error": {
    "code": "FunctionNotFound",
    "message": "No handler registered for 'math.divide'",
    "details": { "target": "math.divide" }
  }
}

Error Codes

The runtime defines these error categories:

Code Meaning
NamespaceNotFound The requested namespace is not registered
FunctionNotFound The requested function does not exist in its namespace
InvalidArguments Arguments failed type or shape validation
IncompatibleVersion Envelope protocol version is incompatible with the runtime
MalformedEnvelope A required field was missing from an envelope
NoProvider No provider is registered for the target namespace
ProviderUnavailable The provider is temporarily unavailable
BatchRoutingConflict A batch item’s target resolved to a different namespace
CapabilityDenied The caller lacks a required capability token
CapabilityInvalid The capability token presented is invalid or expired
ConnectionLost The underlying transport connection was lost
MessageTooLarge A message exceeded the configured size limit (16 MiB)
Timeout The operation exceeded its timeout
BufferOverflow Receive buffer overflowed due to backpressure violation
ProviderError The provider handler returned an explicit error
ProviderPanic The provider panicked while handling the invocation
StreamClosed Stream was already closed when an item was sent
ChannelClosed Channel was closed by the remote side
OutOfOrder Out-of-order sequence number on an ordered stream or channel
Internal An error category not covered by the above codes

Adapter Error Handling

Each adapter maps ErrorDetail to a native error type:

// TypeScript
import { SaikuroClient, SaikuroTimeoutError } from "@nisoku/saikuro";

try {
  const result = await client.call("math.add", [1, 2], { timeoutMs: 1000 });
} catch (err) {
  if (err instanceof SaikuroTimeoutError) {
    console.error("call timed out");
  } else if (err instanceof FunctionNotFoundError) {
    console.error("function not found");
  }
}
# Python
from saikuro import SaikuroError, TransportError

try:
    result = await client.call("math.add", [1, 2])
except TransportError as e:
    print(f"connection failed: {e}")
except SaikuroError as e:
    print(f"saikuro error: {e}")
// Rust
use saikuro::Error;

match client.call::<i32>("math.add", &[1.into(), 2.into()]).await {
    Ok(result) => println!("{result}"),
    Err(Error::Timeout { millis }) => eprintln!("timed out after {millis}ms"),
    Err(e) => eprintln!("error: {e}"),
}
// C#
try {
    int result = await client.CallAsync<int>("math.add", new object[] { 1, 2 });
} catch (SaikuroTimeoutException ex) {
    Console.WriteLine("call timed out");
} catch (SaikuroException ex) {
    Console.WriteLine($"saikuro error: {ex.Message}");
}
// C
char* err = saikuro_last_error_message();
if (err) {
    fprintf(stderr, "Saikuro error: %s\n", err);
    saikuro_string_free(err);
}
// C++
try {
    auto result = client.call_json("math.add", "[1, 2]");
} catch (const saikuro::Error& e) {
    fprintf(stderr, "Saikuro error: %s\n", e.what());
}

Provider Error Handling

Providers signal errors by throwing or returning an error:

// TypeScript provider
provider.register("divide", (a: number, b: number) => {
  if (b === 0) throw new Error("division by zero");
  return a / b;
});
# Python provider
@provider.register("divide")
def divide(a: float, b: float) -> float:
    if b == 0:
        raise ValueError("division by zero")
    return a / b
// Rust provider
provider
    .register("divide", |args: HandlerArgs| -> saikuro::Result<Value> {
        let b: f64 = args.get(1)?;
        if b == 0.0 {
            return Err(saikuro::Error::Provider("division by zero".into()));
        }
        Ok((a / b).into())
    })
    .await?;

The runtime catches the error and returns it to the caller as an ProviderError code with the message preserved.

Panics

If a provider panics (uncaught exception in Python, panic in Rust, unhandled rejection in TypeScript), the runtime catches it and returns ProviderPanic. The provider process remains running and can handle subsequent calls.

Timeouts

Calls can specify a timeout in milliseconds. If the provider does not respond in time, the runtime returns Timeout:

const result = await client.call("math.add", [1, 2], { timeoutMs: 5000 });
result = await client.call("math.add", [1, 2], timeout=5.0)
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var result = await client.CallAsync<int>("math.add", new object[] { 1, 2 }, cts.Token);

Next Steps