KsError
Binds a @Serializable Throwable subclass to a wire-level integer error code on a KsMethod-annotated function.
Declared at the call site with one entry per bindable error type:
@KsMethod("/initialize")
@KsError(code = 100, type = InitError::class)
@KsError(code = 101, type = VersionError::class)
suspend fun initialize(params: InitParams): InitResultThe handler throws an instance of the bound type directly (no wrapper required); ksrpc encodes the throwable using its KSerializer and emits the bound code on the wire. The client deserializes back into the bound type and re-throws — callers catch (e: MyError) typed.
The code on the binding is the single source of truth for the wire code; there is no per-throw override mechanism. If you want a different code for the same data, bind it differently.
The bound type must:
Be
@Serializable(validated by the ksrpc compiler plugin)Extend
Throwable(or any subclass — typicallyRuntimeException)Declare only fields safe to serialize (no
cause, no inheritedstackTrace);messageis typically computed from the serialized fields:
@Serializable
class InitError(val retry: Boolean, val reason: String) : RuntimeException() {
override val message: String get() = "init failed: $reason"
}The server stack trace is NOT propagated across the wire — clients see a stack from local deserialization. Use message and the serialized fields for diagnostic info.
The plugin captures KSerializer<type> and exposes a bidirectional map on the generated RpcMethod descriptor — forward (code → KSerializer) for client-side deserialization, reverse (KClass → code + KSerializer) for server-side resolution of a thrown t::class. Unlike sibling annotations propagated via @KsMethodMetadata, this marker is consumed by the compiler plugin directly because the binding requires a real serializer reference and a dedicated lookup table, not an opaque metadata bag.
Samples
import com.monkopedia.ksrpc.RpcService
import com.monkopedia.ksrpc.annotation.KsError
import com.monkopedia.ksrpc.annotation.KsMethod
import com.monkopedia.ksrpc.annotation.KsService
import com.monkopedia.ksrpc.ksrpcEnvironment
import com.monkopedia.ksrpc.rpcObject
import com.monkopedia.ksrpc.serialized
import com.monkopedia.ksrpc.toStub
import kotlinx.serialization.Serializable
fun main() {
//sampleStart
// @KsError binds a @Serializable Throwable subclass to an integer error code.
// Multiple @KsError annotations can appear on the same method.
// The compiler plugin captures the bidirectional code <-> serializer map.
val rpcObj = rpcObject<DocumentService>()
val createEndpoint = rpcObj.findEndpoint("create")
val getEndpoint = rpcObj.findEndpoint("get")
//sampleEnd
}import com.monkopedia.ksrpc.RpcService
import com.monkopedia.ksrpc.annotation.KsError
import com.monkopedia.ksrpc.annotation.KsMethod
import com.monkopedia.ksrpc.annotation.KsService
import com.monkopedia.ksrpc.ksrpcEnvironment
import com.monkopedia.ksrpc.rpcObject
import com.monkopedia.ksrpc.serialized
import com.monkopedia.ksrpc.toStub
import kotlinx.serialization.Serializable
fun main() {
//sampleStart
// Server implementation throws typed errors directly.
val service = object : DocumentService {
override suspend fun createDocument(content: String): String {
if (content.isBlank()) {
throw ValidationError("content", "must not be blank")
}
return "doc-001"
}
override suspend fun getDocument(id: String): String {
if (!id.startsWith("doc-")) {
throw ValidationError("id", "invalid format")
}
throw NotFoundError(id)
}
}
val env = ksrpcEnvironment { }
val serialized = service.serialized(env)
val stub = serialized.toStub<DocumentService, String>()
// Clients catch the original typed exception after deserialization.
try {
stub.getDocument("doc-missing")
} catch (e: NotFoundError) {
// e.resourceId == "doc-missing"
println("Not found: ${e.resourceId}")
} catch (e: ValidationError) {
println("Validation: ${e.field} - ${e.reason}")
}
//sampleEnd
}