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.
Channel errors
Section titled “Channel errors”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.
VaultUnavailableError
Section titled “VaultUnavailableError”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 bylogin/unlockwhen the per-user vault is unreachable (network / 5xx / rate-limit). Distinct from a wrong password — surface “try again” rather than re-prompting for credentials.
DeviceRevokedError
Section titled “DeviceRevokedError”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 byloginWithDevice/unlockWithDevicewhen thedevicefactor 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.
PaymentRequiredError
Section titled “PaymentRequiredError”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 getuser_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.
Filesystem errors
Section titled “Filesystem errors”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.unlockedtells 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, orsweep()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 ofpathremoves 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).
Other typed errors
Section titled “Other typed errors”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. Seeclient.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). Callclient.auth.zk.unlock(password)or sign in again.IdentityUnavailableError(.code === "identity-unavailable") — the stable chat keys could not be loaded..retryablesays whether trying again can succeed (truefor an unreachable vault;falsefor 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(.codeisnot-memberoridentity-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. Callunlock().DevicePairingError/ReauthRequiredError— hosted device pairing.AuthHttpError(.label,.status,.code) — an auth-service call answered non-2xx; thrown by the lower-levelAuthClient.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— aclient.vcsoperation was refused.
HttpError
Section titled “HttpError”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.
Common cases
Section titled “Common cases”| Where | Message / status contains | Cause & fix |
|---|---|---|
login | commitment mismatch / incorrect password | Wrong password, or a legacy/incompatible account. |
login | User not found | No account with that username. |
login | challenge | Auth challenge expired/replayed — retry. |
login / unlock | VaultUnavailableError | Per-user vault unreachable (not a wrong password) — tell the user to retry. |
loginWithDevice / unlockWithDevice | DeviceRevokedError | The pairing was revoked — discard the stored key, ask for a password. |
| storage | not signed in | No session — login() / restore() first. |
| storage | identity | Encrypted op needs an unlocked identity — login() or unlock(password). |
client.vfs | VfsLockedError | Master seed not in memory (after a reload) — sign in again. |
client.vfs | VfsCorruptRecordError / cannot be read | A directory record this version cannot read — remove it with a recursive delete of err.path. (From 0.14.0-alpha.9.) |
client.vfs | VfsLimitError / VFS: stopped | Tree past a budget or 64 levels deep — err.at says where; remove what you do not recognise. (From 0.14.0-alpha.9.) |
client.vfs | VfsConflictError / a recursive delete removes it | The 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.) |
| message | not signed in | Messaging needs a signed-in user. |
| message | target must be 'user:…' | send() only takes user:<id> targets; use publish() for pub/sub. |
| any request | HttpError 401 — API key required | App key missing/invalid, or minted against a different deployment (keys are per-environment). |
| any request | HttpError 402 | Usage quota exceeded for the app’s tier. |
| writes | PaymentRequiredError | Refused for payment (err.code): paid-only app on a Free user, or a past-due payment. See above. |
client.app.access() | HttpError 404 | The accelerator predates the route. |
Defensive patterns
Section titled “Defensive patterns”// 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);}