Skip to content

Error Handling

Errors thrown in handlers are automatically caught, serialized, and transmitted to the client as Arrow IPC error batches.

Throw any Error from a handler to signal a failure:

import { Protocol, float } from "@query-farm/vgi-rpc";
const protocol = new Protocol("MyService");
protocol.unary("divide", {
params: { a: float, b: float },
result: { result: float },
handler: async ({ a, b }) => {
if (b === 0) throw new Error("Division by zero");
return { result: a / b };
},
});

The error message is transmitted to the client and the transport remains clean for subsequent requests.

Errors are transmitted as zero-row Arrow batches with EXCEPTION-level metadata, carrying the three layers of the error model (WIRE_PROTOCOL.md §8, adopted from gRPC’s google.rpc.Status):

  • vgi_rpc.log_level set to EXCEPTION
  • vgi_rpc.log_message containing the error message ("{ExceptionType}: {message}")
  • vgi_rpc.error_code — always present: one of gRPC’s sixteen canonical code names (UNAVAILABLE, NOT_FOUND, …). An error that declares none is UNKNOWN.
  • vgi_rpc.error_kind — optional. The reason a client branches on, present when the thrown error carries an errorKind marker (see Typed error classes below).
  • vgi_rpc.error_details — optional. A JSON array of typed details from a fixed catalog (vgi_rpc.RetryInfo, vgi_rpc.BadRequest, …). At most 4 KiB of UTF-8; a larger array, or one that repeats a type or invents a vgi_rpc.* type, is dropped whole, never trimmed.
  • vgi_rpc.log_extra — a JSON blob mirroring the three layers:
{
"exception_type": "ServerDrainingError",
"exception_message": "server is draining",
"error_code": "UNAVAILABLE",
"error_kind": "server_draining",
"error_details": [{"@type": "vgi_rpc.RetryInfo", "retry_delay_seconds": 1}],
"traceback": "..."
}

traceback is included by default on every transport — the DuckDB extension shows the remote traceback in the error a user sees. One server-wide switch turns it off everywhere: new VgiRpcServer(protocol, { includeTracebacks: false }) (or the HTTP handler’s includeTracebacks option). When on, it is never empty. Type, message, code, kind and details are sent either way.

Throw a StatusError to choose what a client sees:

import { retryInfo, StatusError } from "@query-farm/vgi-rpc";
throw new StatusError("report is being rebuilt", {
code: "UNAVAILABLE",
kind: "report_rebuilding",
details: [retryInfo(30)],
});

Any error class may instead declare errorCode / errorKind / errorDetails properties (instance or static). Details must come from the catalog; a protocol-specific type lives under the protocol’s own name (acme.Reports.v1.SomeDetail).

Over the HTTP transport the error category is also surfaced as the X-VGI-RPC-Error response header (the RPC_ERROR_HEADER constant), so clients can inspect it without reading the Arrow body.

Represents an error received from the RPC protocol:

import { RpcError } from "@query-farm/vgi-rpc";
class RpcError extends Error {
readonly errorType: string;
readonly errorMessage: string;
readonly remoteTraceback: string; // "" only when the server turned tracebacks off
readonly errorCode: string; // "" only from a server older than the model
readonly errorKind: string; // "" when absent
readonly errorDetails: readonly ErrorDetailJson[]; // as received, unknown types included
readonly requestId: string;
get code(): ErrorCode; // UNKNOWN when absent or unrecognised
isRetryable(): boolean;
details(): ErrorDetail[]; // catalog entries only
retryInfo(): RetryInfo | null; // also errorInfo(), badRequest(), preconditionFailure(),
// quotaFailure(), resourceInfo(), help(), localizedMessage()
}

Every client decode path — pipe, HTTP unary, stream init and exchange, and an externalized error batch — fills these fields. isRetryable() is a classification, not a policy: UNAVAILABLE is retryable, RESOURCE_EXHAUSTED only with RetryInfo, everything else is final. The client never retries an RPC error automatically, because a method may not be idempotent.

Thrown when the client sends an unsupported request version:

import { VersionError } from "@query-farm/vgi-rpc";
class VersionError extends Error {
// Inherits message from Error
}

Several built-in error classes carry a stable errorKind marker that is hoisted onto the error batch as the vgi_rpc.error_kind metadata key (and into the X-VGI-RPC-Error header over HTTP). Throw these from a handler to signal a specific, machine-readable condition. The corresponding ERROR_KIND_* string constants are exported for matching on the client side.

import {
MethodNotImplementedError,
ServerDrainingError,
SessionLostError,
ERROR_KIND_METHOD_NOT_IMPLEMENTED,
ERROR_KIND_SERVER_DRAINING,
ERROR_KIND_SESSION_LOST,
} from "@query-farm/vgi-rpc";
Class errorKind value Constant Meaning
MethodNotImplementedError method_not_implemented ERROR_KIND_METHOD_NOT_IMPLEMENTED The client invoked a method the server does not implement.
ServerDrainingError server_draining ERROR_KIND_SERVER_DRAINING A session was requested while the server is draining.
SessionLostError session_lost ERROR_KIND_SESSION_LOST A sticky session token is malformed, expired, evicted, or bound to a different worker / principal (HTTP only).

ProtocolVersionError extends VersionError and is thrown by the server when the client’s declared vgi_rpc.protocol_version is incompatible with the server’s (exact major + minor match required; patch ignored). It carries the protocol_version_mismatch error kind and a directional message indicating which side to upgrade. Because it subclasses VersionError, existing catch sites continue to write a typed error stream and keep serving. It is not exported from the package index — it is produced internally by the server.

Errors in producer produce and exchange exchange functions terminate the stream and send an error batch to the client. The init function errors are also caught and reported.