Skip to content

Errors

SDK methods reject with a standard Error whose message identifies the case. HTTP failures throw an HttpError carrying the status. Channel operations and the filesystem throw typed errors you can branch on.

import { ChannelNotFoundError, ChannelExistsError } from "@muhkoo/connect";
try {
await client.space.joinChannel("project-x");
} catch (err) {
if (err instanceof ChannelNotFoundError) {
// No channel with that name — offer to create it.
await client.space.createChannel("project-x");
}
}
  • ChannelNotFoundError — joinChannel(name) when the name isn’t registered. (carries .channelName.)
  • ChannelExistsError — createChannel(name) when the name is already taken — join it instead.
import { VaultUnavailableError } from "@muhkoo/connect";
try {
await client.auth.zk.login("ada", pw);
} catch (err) {
if (err instanceof VaultUnavailableError) {
// Vault unreachable — tell the user to retry, don't blame the password.
}
}
  • VaultUnavailableError — thrown by login / unlock when the per-user vault is unreachable (network / 5xx / rate-limit). Distinct from a wrong password — surface “try again” rather than re-prompting for credentials.
import { DeviceRevokedError } from "@muhkoo/connect";
try {
await client.auth.zk.loginWithDevice(username, factorId, deviceKey);
} catch (err) {
if (err instanceof DeviceRevokedError) {
discardStoredKey(); // it can never work again
showPasswordForm(); // the only way back in
}
}
  • DeviceRevokedError — thrown by loginWithDevice / unlockWithDevice when the device factor backing a paired browser or machine is no longer in the vault. Typed because the right response is specific and the wrong one is harmful: discard the stored key and fall back to a password. Retrying cannot succeed, and re-pairing silently would undo a revocation someone performed on purpose. A wrong key is a different failure — it fails to decrypt rather than to resolve.
import { PaymentRequiredError } from "@muhkoo/connect";
try {
await client.kv.set("notes", id, note);
} catch (err) {
if (err instanceof PaymentRequiredError) {
if (err.upgradeUrl) location.href = err.upgradeUrl;
else showBanner(err.code);
}
}

A write the accelerator refused for payment. It extends HttpError (so status is 402 and body is the parsed response) and is thrown for a 402 whose body carries one of three codes, in .code:

  • user_plan_required — the app takes paid Muhkoo plans only and the user is on Free.
  • user_payment_past_due — the user’s own payment is past due. Only first-party apps are told this; other apps get user_plan_required.
  • owner_payment_past_due — the app owner’s payment is past due, so the app stores nothing new for anyone.

.upgradeUrl is the portal page to send the user to, set only when the server sent an absolute URL (never for owner_payment_past_due: the user can do nothing about it). Reads, sign-in and deletes still work in all three cases. Ask ahead of time with client.app.access().

It is thrown by writes that add data through client.kv, client.db, client.vfs, client.space.createSpace and file uploads. It is not thrown for a refusal sent over a Space’s live socket, for a Space’s keyring requests, or by client.functions.invoke (which returns the raw Response). Every other 402, such as a Free-tier quota refusal, stays a plain HttpError. The offline queue keeps a write refused this way and retries it later — see client.offline. Exported from every build.

VfsCorruptRecordError and VfsLimitError land in 0.14.0-alpha.9, and the sample below needs that release: on 0.14.0-alpha.8 neither is exported. On earlier versions the same records surfaced as a TypeError, a SyntaxError or a bare decryption error, and a walk past 64 levels returned part of the tree.

import { VfsLockedError, VfsCorruptRecordError, VfsLimitError } from "@muhkoo/connect";
try {
await client.vfs.walk("/apps/my-app");
} catch (err) {
if (err instanceof VfsLockedError) {
await client.auth.zk.resume(); // the seed is gone; sign in again
} else if (err instanceof VfsCorruptRecordError) {
offerToRemove(err.path, err.id); // a recursive delete removes it
} else if (err instanceof VfsLimitError) {
showTooLarge(err.limit, err.at); // which ceiling, and where
}
}

client.vfs throws these. Any app a user signs into holds their master seed and can write any record in their filesystem, so the SDK treats what it reads there as untrusted input — see Reading an untrusted filesystem. A record or tree it cannot safely read throws one of the last two, rather than reading as empty or as part of a tree, because a sync reads a missing path as a deleted one.

  • VfsLockedError — the master seed is not in memory, which a reload causes. Sign in again; client.vfs.unlocked tells you in advance.
  • VfsNotFoundError — the path does not exist, or is held only by an entry this version cannot read.
  • VfsConflictError — the operation would clobber or misuse an entry: the destination exists, a directory is not empty, a file’s history cannot be opened, or sweep() would have to guess what is unreachable.
  • VfsCorruptRecordError — a directory record does not decrypt, or is not a shape this version reads. Carries .id (the record’s id, raw — escape it before printing) and .path (the path it was reached through, when known). The record is left exactly as stored; a recursive delete of path removes it, except for the root, which cannot be deleted.
  • VfsLimitError — a walk or copy would pass a budget (500,000 entries, 32,000,000 characters of path) or 64 levels of depth, or a write would put a directory more than 64 levels below the root. Carries .limit ("entries", "pathChars", "depth" or "shared"), .max, .path (where the operation started, or a write’s destination) and .at (where the ceiling was reached).

All are exported from @muhkoo/connect (browser and Node builds).

  • VaultProtocolMismatchError — the vault refuses the OPRF protocol this SDK speaks. Update the app; retrying never helps. See client.auth.
  • PasskeyOriginError (.code === "passkey_wrong_origin") — the account’s passkey belongs to a different origin. Fall back to another factor.
  • IdentityLockedError (.code === "identity-locked") — a Space needs the signed-in user’s seed and the session is locked (typically after a reload). Call client.auth.zk.unlock(password) or sign in again.
  • IdentityUnavailableError (.code === "identity-unavailable") — the stable chat keys could not be loaded. .retryable says whether trying again can succeed (true for an unreachable vault; false for saved keys that do not unwrap under this seed).
  • SpaceTrustError (.code, .detail) — a membership-log or pin failure. Codes: integrity, log-conflict, log-refused, lookup-failed, pins-missing, pins-rollback, pins-stale, pins-invalid, pins-write-failed.
  • AdmissionDeniedError (.code is not-member or identity-mismatch) — this user may not open this Space.
  • OfflineLockedError — an offline read or write needs the identity to decrypt cached data, but the client is locked. Call unlock().
  • DevicePairingError / ReauthRequiredError — hosted device pairing.
  • AuthHttpError (.label, .status, .code) — an auth-service call answered non-2xx; thrown by the lower-level AuthClient.
  • CircuitIntegrityError (.url, .expected, .actual) — a ZK circuit asset did not match its pinned SHA-256 digest. Proving is not attempted.
  • KeyringHttpError (.status, .body) — a Space keyring route answered non-2xx.
  • DeviceStoreUnavailableError — the paired-device identity can’t be persisted (no IndexedDB / localStorage); pairing still works but must be repeated after a restart.
  • VcsError — a client.vcs operation was refused.
import { HttpError } from "@muhkoo/connect";
try {
await client.kv.get("todos", "t1");
} catch (err) {
if (err instanceof HttpError) {
console.log(err.status); // e.g. 401, 402, 404
console.log(err.body); // parsed JSON error body, if any
}
}

PaymentRequiredError is a subclass, so check for it first if you handle both.

WhereMessage / status containsCause & fix
logincommitment mismatch / incorrect passwordWrong password, or a legacy/incompatible account.
loginUser not foundNo account with that username.
loginchallengeAuth challenge expired/replayed — retry.
login / unlockVaultUnavailableErrorPer-user vault unreachable (not a wrong password) — tell the user to retry.
loginWithDevice / unlockWithDeviceDeviceRevokedErrorThe pairing was revoked — discard the stored key, ask for a password.
storagenot signed inNo session — login() / restore() first.
storageidentityEncrypted op needs an unlocked identity — login() or unlock(password).
client.vfsVfsLockedErrorMaster seed not in memory (after a reload) — sign in again.
client.vfsVfsCorruptRecordError / cannot be readA directory record this version cannot read — remove it with a recursive delete of err.path. (From 0.14.0-alpha.9.)
client.vfsVfsLimitError / VFS: stoppedTree past a budget or 64 levels deep — err.at says where; remove what you do not recognise. (From 0.14.0-alpha.9.)
client.vfsVfsConflictError / a recursive delete removes itThe name is held by an entry this SDK version cannot read — upgrade, or remove it with a recursive delete. (From 0.14.0-alpha.9.)
messagenot signed inMessaging needs a signed-in user.
messagetarget must be 'user:…'send() only takes user:<id> targets; use publish() for pub/sub.
any requestHttpError 401 — API key requiredApp key missing/invalid, or minted against a different deployment (keys are per-environment).
any requestHttpError 402Usage quota exceeded for the app’s tier.
writesPaymentRequiredErrorRefused for payment (err.code): paid-only app on a Free user, or a past-due payment. See above.
client.app.access()HttpError 404The accelerator predates the route.
// Restore on boot without throwing on a stale token:
const user = await client.auth.zk.restore().catch(() => null);
// Gate encrypted reads on an unlocked identity:
if (client.auth.zk.identity) {
const data = await client.kv.get("notes", id);
}