Skip to main content

Handle errors by code

Your program needs to react to a runtime failure without parsing prose. Every ClikaRT failure carries the same three channels, in every language: a coarse status (the policy switch: retry, reject, fail), a human message (diagnostics for a log line), and a stable code name (the fine-grained machine channel, e.g. TIMED_OUT, BACKEND_NOT_LOADED). The rule this page exists for: branch on the code name, never on the message text. A failure you can act on (a bad shape, an option out of range, a missing file) reads as plain text in every build and names the operation and the values it refused. A message of the form E<digits> means a fault inside the runtime: its value is specific to the build that produced it, so it is nothing to record, compare or branch on; report it verbatim with the runtime version. The code name is machine-readable in every build.

Read the three channels

The program provokes one failure (a shape-mismatched matmul) and reads everything the error carries. The C samples abbreviate the api-table bootstrap that tutorial part 1 shows in full.

three_channels.cpp
#include <cstdio>

#include "ClikaRT/clika_rt.h"

using ClikaRT::DataType;
using ClikaRT::Error;
using ClikaRT::Status;
using ClikaRT::Tensor;
namespace ops = ClikaRT::ops;

int main() {
const Tensor a = Tensor::ones({3, 4}, DataType::Float32);
const Tensor bad = Tensor::ones({5, 5}, DataType::Float32);

try {
const Tensor y = ops::matmul(a, bad);
} catch (const Error& e) {
// The three channels of every runtime failure.
std::printf("status: %d\n", static_cast<int>(e.status()));
std::printf("message: %s\n", e.what());
std::printf("code name: %s\n", e.code_name().c_str());

// Branch on the code NAME: stable in every build, and the only one of
// the three to compare against. A failure you can act on, like this
// one, states the shapes in plain text in every build; a message of
// the form E<digits> means a defect inside the runtime, and its value
// is specific to the build that produced it.
if (e.code_name() == "INVALID_ARGUMENT")
std::printf("-> reject this request, keep serving\n");

// The coarse status is the POLICY switch.
switch (e.status()) {
case Status::Unavailable: /* shed under load: retry later */ break;
case Status::InvalidArgument: /* caller bug: answer 400 */ break;
default: /* fail loudly */ break;
}
}
return 0;
}
status: 1
message: ops::matmul: cannot contract a [3, 4] with b [5, 5]: a's last dim (4) must equal b's dim 0 (5)
code name: INVALID_ARGUMENT
-> reject this request, keep serving

Status 1 is InvalidArgument, the same value in the C++ Status enum and the C clika_rt_status (the C surface freezes its numbering append-only). The message names the operation and the two shapes it refused, in a release build as in a debug build. Python is the one variation: it folds all three channels into RuntimeError, the message carrying the code name as a bracketed [code: NAME] suffix; the twelve-line helper above is the whole extraction.

Branch on the code name, never the message

The message exists for a human reading a log. An argument mistake reads as a sentence that names the operation and the values, in every build, but its wording can change between versions, so it is not a contract. A message of the form E<digits> is an internal fault code: it stands for a defect inside the runtime, its value is specific to the build that produced it (it changes between releases), and it is not an identifier to record, compare or branch on. Report it verbatim together with the runtime version; it decodes on the release side. The code name is the contract: a stable, append-only name for the fine-grained status the failure originated with, readable in every build flavor. An empty code name is meaningful too: the failure did not originate inside the runtime (the C++ Error from your own code, a client-side load failure), so there is nothing machine-readable to branch on.

A placement failure names its reason

One message with a fixed shape: placing work on a device whose backend did not come up fails with the reason spelled out, cannot allocate on device <name>: <why>. The <why> names what actually happened: the backend library is not shipped beside the runtime, it failed to load (with the loader's cause), or it loaded and brought up no device. The code name stays the branch point; the message is now worth logging as is. On a machine without the Metal backend:

backend_reason.cpp
#include <cstdio>

#include <ClikaRT/clika_rt.h>

using ClikaRT::DataType;
using ClikaRT::Device;
using ClikaRT::Error;
using ClikaRT::Tensor;

int main() {
try {
const Tensor t = Tensor::ones({2, 2}, DataType::Float32, Device::metal());
std::printf("placed: %s\n", t.to_string().c_str());
} catch (const Error& e) {
std::printf("status: %d\n", static_cast<int>(e.status()));
std::printf("message: %s\n", e.what());
std::printf("code name: %s\n", e.code_name().c_str());
}
return 0;
}
status: 4
message: cannot allocate on device Metal:0: The metal backend could not load: not shipped in this install. The metal backend is not part of this ClikaRT build.
code name: BACKEND_NOT_LOADED

The same reason text reaches every binding through its language's error channel, and clika-modelverse prints it when --device names a backend that cannot come up.

Switch policy on the coarse status

The code name says what happened; the coarse status says what KIND of thing happened, which is usually all a policy needs:

StatusMeaningUsual reaction
InvalidArgumentthe caller's request cannot be rightreject it (a 400 in the serving guide)
NotFounda named thing does not existreject or fall back (a 404)
Unsupportedthis build or backend cannot do itfail fast at startup, not per request
Internalthe runtime's own invariant brokelog everything, file it
Unavailabletransient: the work was shed under loadretry later (a 503)

The C surface adds one protocol-level status of its own, BUFFER_TOO_SMALL, which is not a failure at all: it drives the sized-string two-call protocol every C string read uses.

Without exceptions

C++ callers that would rather not pay for exceptions read the same three channels off a Result<T>: ok(), status(), message(), code_name(). CLIKART_TRY(...) captures a throwing call as a Result where a failure is an expected outcome; the serving guide uses it for exactly that on its 400 paths. The C samples never throw by construction: every fallible api-table member returns the error object directly.

The serving-side nn::KVCache is a worked example of the Result surface: its per-layer accessors (keys_impl, values_impl, conv_state_impl, recurrent_state_impl) return a failed Result for a layer index outside [0, num_layers()) instead of crashing, and num_layers() reports the count without allocating:

kv_bounds.cpp
#include <cstdio>

#include <ClikaRT/clika_rt.h>

using ClikaRT::Result;
using ClikaRT::Tensor;
namespace nn = ClikaRT::nn;

int main() {
// The smallest real cache: two full-attention layers, one slot.
nn::KVCacheConfig config;
config.num_layers = 2;
config.num_kv_heads = 1;
config.head_dim = 4;
config.max_seqs = 1;
config.max_tokens_per_seq = 8;
const nn::KVLayerSpec specs[2] = {}; // AttentionKV rows by default

const nn::KVCache cache = nn::KVCache::make_impl(config, specs).value_or_throw();

// The per-layer accessors return a failed Result for a layer outside
// [0, num_layers()); no exception, no crash.
std::printf("layers: %d\n", cache.num_layers());
const Result<Tensor> in_range = cache.keys_impl(1);
const Result<Tensor> out_range = cache.keys_impl(2);
std::printf("keys(1): ok=%s\n", in_range.ok() ? "true" : "false");
std::printf("keys(2): ok=%s, message: %s, code name: %s\n",
out_range.ok() ? "true" : "false",
out_range.message().c_str(), out_range.code_name().c_str());
return 0;
}
layers: 2
keys(1): ok=true
keys(2): ok=false, message: kv cache layer 2 is out of range: the cache has 2 layer(s), code name: INVALID_ARGUMENT

Tutorial part 1 introduces the error model this page operationalizes; the python examples' errors chapter walks the same contract with an oracle per claim.