Skip to content

Errors + ETags

Every SDK method that hits the network throws on non-2xx (except where explicitly noted). Errors are normalised into VaultbaseError with a discriminated kind field.

import { VaultbaseError, isVaultbaseError } from "@vaultbase/sdk";
try {
await vb.collection("posts").create({ title: "" });
} catch (e) {
if (isVaultbaseError(e)) {
e.kind; // "validation" | "forbidden" | "not_found" | …
e.status; // HTTP status
e.message; // server's `error` field
e.details; // server's `details` map (validation only)
e.etag; // current server tag (precondition_failed only)
}
throw e;
}
kindStatusMeaning
bad_request400Malformed input the SDK didn’t catch
unauthenticated401Missing / invalid token, expired
forbidden403Rule denied access
not_found404Collection or record missing
gone410One-time file token replayed
conflict409Unique violation
precondition_failed412ETag mismatch — see optimistic concurrency below
unsupported_media415MIME outside the global allow-list
validation422Schema validation; see details
rate_limited429Token bucket exhausted
server_error500Unexpected server failure
networkTCP / DNS / TLS failure (no HTTP exchange)
abortedAbortController triggered or auto-cancel
timeoutPer-request timeout exceeded

Vaultbase emits an ETag header on single-record GET / POST / PATCH responses. The SDK caches it per (collection, id) and attaches If-Match: <tag> on the next mutation automatically.

When two clients race:

// Client A
const post = await vb.collection("posts").get("p_42"); // tag W/"42"
// Client B updates same record → server's tag is now W/"43"
await vb.collection("posts").update("p_42", { title: "x" });
// → throws VaultbaseError, kind: "precondition_failed", etag: "W/\"43\""

Recovery strategies:

import { isVaultbaseError } from "@vaultbase/sdk";
async function safeUpdate() {
for (let i = 0; i < 3; i++) {
try {
return await vb.collection("posts").update("p_42", { title: "x" });
} catch (e) {
if (!isVaultbaseError(e) || e.kind !== "precondition_failed") throw e;
// Re-read with the fresh tag and retry
await vb.collection("posts").get("p_42");
}
}
throw new Error("Concurrency contention — gave up after 3 retries");
}
// Use an explicit ETag (e.g. from a prior response):
await vb.collection("posts").update("p_42", { title: "x" }, { ifMatch: 'W/"42"' });
// Disable optimistic concurrency for this call:
await vb.collection("posts").update("p_42", { title: "x" }, { ifMatch: false });
// Force "auto" (default) explicitly:
await vb.collection("posts").update("p_42", { title: "x" }, { ifMatch: "auto" });
vb.client.etags.get("posts", "p_42"); // → string | undefined
vb.client.etags.set("posts", "p_42", 'W/"42"'); // manual seed
vb.client.etags.delete("posts", "p_42");
vb.client.etags.clear(); // wipe all

The cache is in-memory per Vaultbase instance — multiple Vaultbase objects don’t share state.

try {
await vb.collection("posts").create({});
} catch (e) {
if (isVaultbaseError(e) && e.kind === "validation") {
e.details;
// → { title: "title is required", author: "author is required" }
}
}

details is a { field → message } map suitable for binding directly to form-field error UI.

When the server returns 429, VaultbaseError.kind = "rate_limited". The Retry-After header is exposed via e.retryAfterSeconds.

try {
await vb.collection("posts").list();
} catch (e) {
if (isVaultbaseError(e) && e.kind === "rate_limited") {
await new Promise((r) => setTimeout(r, e.retryAfterSeconds! * 1000));
// …retry
}
throw e;
}

kind: "network" covers anything that didn’t reach the server: DNS failure, TLS error, connection refused, browser offline.

if (isVaultbaseError(e) && e.kind === "network") {
showOfflineBanner();
}
  • Always narrow with isVaultbaseError(e) before reading typed fields.
  • Don’t retry validation / forbidden / not_found — they won’t resolve themselves.
  • Do retry precondition_failed after re-reading.
  • Do retry network / timeout / rate_limited with exponential backoff (capped) — but only for idempotent ops.
  • Never retry POST create blindly without an idempotency key (server issues UUIDs, so duplicate creates are visible as two records).