Skip to main content

Error handling

Zaris operations return a structured KvResult instead of throwing for expected outcomes. You inspect the result's status and metadata to decide what to do next. Exceptions are reserved for genuinely unexpected failures.

The result model

Every operation returns a KvResult, or a KvResult<T> when it produces a value. The result describes the full outcome, not just success or failure.

var result = await client.PutAsync("key", "value");

Because expected conditions — a missing key, a concurrency conflict, a locked resource — come back as status values rather than exceptions, you handle them with normal control flow instead of try/catch.

Core properties

A KvResult exposes the outcome through a few properties you check on every call.

Status reports the outcome as a KvStatus value. Common values are Success, NotFound, Conflict, InvalidInput, Unavailable, TransportError, and Locked.

result.Status

IsSuccess is a derived helper meaning the status is Success and there is no error. It is the quickest correctness gate.

if (result.IsSuccess)
{
// operation succeeded
}

Error holds the error message when the operation failed. Use it for logging and diagnostics.

result.Error

Reading a typed value

KvResult<T> adds a Value. That value is meaningful only when the operation succeeded, so check IsSuccess before you read it.

var result = await client.GetAsync<int>("key");

if (result.IsSuccess)
{
Console.WriteLine(result.Value);
}
warning

Read Value only after confirming IsSuccess. On a failed or NotFound result the value is not populated, and using it treats a missing or errored read as if it returned real data. Gate every Value access behind a success check.

Handle outcomes by status

For anything beyond a simple success check, branch on Status. Each status calls for a different response, so map them deliberately rather than treating every non-success the same way.

StatusWhen it occursHow to handle
NotFoundThe key does not existBranch on your logic; often not an error
ConflictOptimistic concurrency failed — an IfMatch did not match, or a transaction commit lost a raceReread and retry
LockedThe resource is locked by another ownerBack off and retry
InvalidInputThe request itself is wrongFix the code or input; do not retry
Unavailable / TransportErrorA transient infrastructure or connectivity problemRetry with backoff

The following snippets show the checks for the cases you handle most.

if (result.Status == KvStatus.NotFound)
{
// key does not exist
}
if (result.Status == KvStatus.Conflict)
{
// retry logic
}
if (result.Status == KvStatus.Unavailable ||
result.Status == KvStatus.TransportError)
{
// transient issue → retry
}

Mutation semantics

For writes, the result also tells you what actually changed. This lets you distinguish a create from an update and detect no-op writes.

Mutated is true when the operation actually changed stored state.

if (result.Mutated)
{
// state actually changed
}

IsUpdate is true when an existing value was overwritten, as opposed to a new value being created.

if (result.IsUpdate)
{
// existing value was overwritten
}

Together they let a single PutAsync report whether it created or updated the key.

var result = await client.PutAsync("key", "value");

if (result.IsSuccess)
{
if (result.IsUpdate)
Console.WriteLine("Updated existing value");
else
Console.WriteLine("Created new value");
}

Concurrency, TTL, and metadata

The result carries the data you need for safe follow-up operations.

Version is the entry's current version, used for optimistic concurrency. Capture it and pass it to a later IfMatch write to update the entry only if no one else changed it first.

var version = result.Version;

TimeToLive and ExpiryUtc report the remaining lifetime and absolute expiry, which are useful for monitoring and debugging.

var ttl = result.TimeToLive;
var expiry = result.ExpiryUtc;

Metadata exposes entry metadata including labels, entity, and lease information.

var metadata = result.Metadata;

Retry strategy

Retry only failures that a retry can actually fix: transient infrastructure problems and lost concurrency races. Retrying anything else wastes work or masks a real bug.

This loop retries a conditional write on Conflict and stops on any other outcome. Rereading the current version between attempts (not shown) is what lets a retry eventually succeed.

for (int i = 0; i < 3; i++)
{
var result = await client.PutAsync(
"key",
"value",
Put.WithIfMatch(version));

if (result.IsSuccess)
break;

if (result.Status != KvStatus.Conflict)
break;

await Task.Delay(50);
}

Retry on Conflict, Locked, Unavailable, and TransportError. Do not retry on InvalidInput, on logical or business errors, or on NotFound unless your logic specifically expects the key to appear.

warning

Never retry InvalidInput. It means the request is malformed, so every retry fails the same way and only adds load. Fix the input or the code instead.

Results compared with exceptions

Expected outcomes come back as a KvResult; only unexpected failures throw. Handle both paths.

ScenarioBehavior
Expected outcome (not found, conflict, locked)Returns a KvResult
Unexpected failureThrows an exception

Exceptions surface for cases such as serialization failures, invalid arguments, and internal system errors. Check the result on every call, and let a try/catch cover the exceptional path.

Best practices

These habits keep error handling correct.

  • Check IsSuccess or Status on every operation.
  • Use Status for precise, case-by-case handling.
  • Retry only transient and conflict failures, with backoff.
  • Use Version with IfMatch for safe conditional updates.
  • Use Mutated and IsUpdate to tell what actually changed.

Next steps