Skip to content

Every error

Every error a @fkn/lib 0.9.28 call can produce has a row on this page, and a few rows list more than one wording of the same error. This page groups the rows by the calls that produce them and says, for each, what happened, what to do and whether a retry can succeed.

handling errors explains how to match a row. It also lists which fields of an error survive the hop out of the broker, the connection your app holds into FKN.

The handler below sorts an error into five branches by the shape it arrives in. The guards overlap, so test them in this order:

app.ts
import {
(alias) namespace cloud
import cloud
cloud
,
const isLocatorDenied: (error: unknown) => boolean
isLocatorDenied
,
const isTerminalError: (error: unknown) => boolean
isTerminalError
} from '@fkn/lib'
import {
const isNotFound: (error: unknown) => boolean

Whether an error from this module means the path is empty, as opposed to unreadable.

isNotFound
,
class StorageLockedError
StorageLockedError
} from '@fkn/lib/cloud/fs'
import {
const E2E_INTEGRITY_MESSAGE: "fkn:e2e-integrity: stored data failed its integrity check"
E2E_INTEGRITY_MESSAGE
,
const E2E_STALE_EPOCH_MESSAGE: "fkn:e2e-stale-epoch: this file is encrypted under a previous key you reset"
E2E_STALE_EPOCH_MESSAGE
} from '@fkn/lib/messages'
import {
class BrokerUnreachableError
BrokerUnreachableError
} from '@fkn/lib/api'
const
const handle: (error: unknown) => Promise<null> | Promise<string | Buffer<ArrayBufferLike>> | null
handle
= (
error: unknown
error
: unknown) => {
// absence and locked, a code and a class, both minted in your own realm
if (
function isNotFound(error: unknown): boolean

Whether an error from this module means the path is empty, as opposed to unreadable.

isNotFound
(
error: unknown
error
)) return null
if (
error: unknown
error
instanceof
class StorageLockedError
StorageLockedError
) return
const promptUnlock: () => Promise<null>
promptUnlock
()
// everything else from the broker, by message prefix
const
const message: string
message
=
error: unknown
error
instanceof
var Error: ErrorConstructor
Error
?
error: Error
error
.
Error.message: string
message
: ''
if (
const message: string
message
.
String.startsWith(searchString: string, position?: number): boolean

Returns true if the sequence of elements of searchString converted to a String is the same as the corresponding elements of this object (converted to a String) starting at position. Otherwise returns false.

startsWith
(
const E2E_STALE_EPOCH_MESSAGE: "fkn:e2e-stale-epoch: this file is encrypted under a previous key you reset"
E2E_STALE_EPOCH_MESSAGE
)) throw
error: unknown
error
// never retry, never overwrite
if (
const message: string
message
.
String.startsWith(searchString: string, position?: number): boolean

Returns true if the sequence of elements of searchString converted to a String is the same as the corresponding elements of this object (converted to a String) starting at position. Otherwise returns false.

startsWith
(
const E2E_INTEGRITY_MESSAGE: "fkn:e2e-integrity: stored data failed its integrity check"
E2E_INTEGRITY_MESSAGE
)) throw
error: unknown
error
// never overwrite
if (
const message: string
message
.
String.startsWith(searchString: string, position?: number): boolean

Returns true if the sequence of elements of searchString converted to a String is the same as the corresponding elements of this object (converted to a String) starting at position. Otherwise returns false.

startsWith
('storage: api unreachable')) return
const keepLocalCopy: () => null
keepLocalCopy
()
if (
const message: string
message
.
String.startsWith(searchString: string, position?: number): boolean

Returns true if the sequence of elements of searchString converted to a String is the same as the corresponding elements of this object (converted to a String) starting at position. Otherwise returns false.

startsWith
('FKN: the broker was replaced')) return
const retryOnce: () => Promise<string | Buffer>
retryOnce
()
// locators and frames, by name, through the exported guards
if (
function isLocatorDenied(error: unknown): boolean
isLocatorDenied
(
error: unknown
error
)) return
const explainRefusal: () => null
explainRefusal
()
if (
function isTerminalError(error: unknown): boolean
isTerminalError
(
error: unknown
error
)) return
const giveUp: () => null
giveUp
()
// the consent refusal, its own name, matched by neither guard above
if (
error: unknown
error
instanceof
var Error: ErrorConstructor
Error
&&
error: Error
error
.
Error.name: string
name
=== 'PermissionDeniedError') return
const explainRefusal: () => null
explainRefusal
()
// the broker deadline, a real class, minted in this realm
if (
error: unknown
error
instanceof
class BrokerUnreachableError
BrokerUnreachableError
) return
const waitAndRetry: () => Promise<string | Buffer>
waitAndRetry
()
throw
error: unknown
error
}
const
const catalog: string | Buffer<ArrayBufferLike> | null
catalog
= await
(alias) namespace cloud
import cloud
cloud
.
namespace cloud_d_exports.fs
export cloud_d_exports.fs
fs
.
const fs_d_exports.promises: {
readFile: (path: import("node:fs").PathLike, options?: ReadOptions) => Promise<Buffer | string>;
writeFile: (path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions) => Promise<void>;
unlink: (path: import("node:fs").PathLike) => Promise<void>;
rename: (from: import("node:fs").PathLike, to: import("node:fs").PathLike) => Promise<void>;
readdir: (path: import("node:fs").PathLike) => Promise<string[]>;
mkdir: (_path?: import("node:fs").PathLike, _options?: MakeOptions) => Promise<void>;
... 4 more ...;
access: (path: import("node:fs").PathLike) => Promise<void>;
}
export fs_d_exports.promises
promises
.
readFile: (path: import("node:fs").PathLike, options?: ReadOptions) => Promise<Buffer | string>
readFile
('library/catalog.json', 'utf8').
Promise<string | Buffer<ArrayBufferLike>>.catch<string | Buffer<ArrayBufferLike> | null>(onrejected?: ((reason: any) => string | Buffer<ArrayBufferLike> | PromiseLike<string | Buffer<ArrayBufferLike> | null> | null) | null | undefined): Promise<string | Buffer<ArrayBufferLike> | null>

Attaches a callback for only the rejection of the Promise.

@paramonrejected The callback to execute when the Promise is rejected.

@returnsA Promise for the completion of the callback.

catch
(
const handle: (error: unknown) => Promise<null> | Promise<string | Buffer<ArrayBufferLike>> | null
handle
) // the catalog, or null when nothing is stored yet

On a first run nothing is stored yet, so the read resolves to null. The closing throw passes on a PackagesError or a node-style storage code. Both are matched on code, as their sections below show.

The same handler serves every call site. A storage read never reaches the locator, consent or broker-deadline branches. A realm is one JavaScript execution context, such as a window, a worker or a package tenant. handling errors lists which errors cross out of one.

Type any part of a message, a name, a call or an instruction to narrow every table at once:

Each row has an anchor made from the fixed prefix of its message, the part you match on. /errors/#storage-api-unreachable keeps working for as long as that prefix does. When two rows share a prefix, the later row carries an anchor of its own, as Permission denied: <key> (<scope>) does.

The root fetch, cloud.fetch and extension.fetch produce these errors, and so do the two header-rule calls, extension.setRequestHeaderRule and extension.removeRequestHeaderRule:

MessageName or codeSurfaced byWhat happenedWhat to doRetryable
fetch refuses FKN platform domains (<hostname>)Errorfetch, cloud.fetchThe target hostname is fkn.app, fkn.dev, sdbx.app or any dot-anchored subdomain of them. A trailing dot is stripped first, so fkn.app. and fkn.app%2e are the same name.Point the call at the app's own api. A capability that carries the user's identity is never aimed at a platform origin.No
fetch: refusing to target the extension's own pages or FKN platform domainsErrorextension.fetch, and the root fetch whenever the extension backend is chosenThe url scheme is chrome-extension: or moz-extension:, or the hostname is a platform host. The extension checks this a second time in its content script, so calling the bridge directly does not skip it.Point the call at the app's own api, as for the row above.No
fetch with credentials needs the FKN extension, which only exists in window realmsErrorroot fetch with credentials: 'include'The realm has no document, so there is no content script to reach and no session to spend.Do the credentialed call on the main thread, or drop to credentials: 'omit' and let the cloud backend serve it.No
The FKN WebExtension is not installed, enabled or not exposed on this page.Errorextension.fetch, root fetch with credentials: 'include', extension.attachFrame, attachFrame, cookies.get, permissions.request, setRequestHeaderRule, removeRequestHeaderRule: everything that goes through the extension bridgeThe extension marker data-fkn-extension did not appear within 1000 ms plus a 150 ms grace after load, and the install prompt did not produce a usable extension. In a worker realm this arrives as a ReferenceError instead, because MutationObserver and document do not exist there.Offer promptInstall(reason) or a link to the store listing, then retry once the user has installed or enabled it.Yes, once the user installs or enables it
fetch: refusing to forge request header(s): <names>Errorextension.fetchThe init.headers carried a header outside the forgeable set origin, referer, cookie. <names> is the deduplicated lowercase list.Send only the three forgeable names through that slot. Every other header is a normal request header and goes through untouched.No
FKN cloud.fetch: no proxy is available (the relay directory could not be read, and no fallback origin is configured)Errorcloud.fetch, root fetch on the cloud pathThe ranked relay directory produced no proxy origin, and the published build carries no fallback origin.Retry after a short backoff. The endpoint cache lives 30000 ms and a cooled relay is retried after 60000 ms.Yes
request rule <id> was not issued to this documentErrorremoveRequestHeaderRule(ruleId)The rule id was never issued to this frame, or was issued for a different rule kind.Remove only the ids your own setRequestHeaderRule returned.No
Whatever new Request(input, init) throws, typically a TypeErrorTypeErrorextension.fetchThe extension path normalises through a real Request first, so a GET with a body, or a ReadableStream body without duplex: 'half' on Chromium, throws before anything is sent. The text is the browser's.Fix the init so the browser accepts it.No
Permission denied: network.fetchCredentialed (<scope>), Permission denied: network.fetchLocal (<scope>)PermissionDeniedError, with permissionKey and scopeextension.fetch, root fetch with credentials: 'include'The user refused, or a stored deny covers the scope.Match error.name === 'PermissionDeniedError'; neither isLocatorDenied nor isTerminalError matches it. See Permissions and consent.Yes, if the user changes their mind
400 {"error": "fkn-proxy-protocol must be http or https"}No error: the Response resolves with ok: falsecloud.fetch, and the root fetch on the cloud pathThe target url scheme is neither http nor https.Use an http or https url. Check response.ok and read error from the JSON body.No
400 {"error": "missing fkn-proxy-hostname"}No error: the Response resolves with ok: falsecloud.fetch, and the root fetch on the cloud pathNo hostname reached the proxy.Pass an absolute url that carries a hostname.No
403 {"error": "proxying FKN platform domains is not allowed"}No error: the Response resolves with ok: falsecloud.fetch, and the root fetch on the cloud pathThe proxy keeps its own copy of the platform host list (fkn.app, fkn.dev, dot-anchored). The library and the broker already refuse sdbx.app before the request leaves, so the server list is shorter by one suffix.Point the request at the app's own api; a platform host is never proxied.No
403 {"error": "egress refused (non-public target)"}No error: the Response resolves with ok: falsecloud.fetch, and the root fetch on the cloud pathEvery resolved address of the target is checked and one was in a private, loopback, link-local, multicast or documentation range. The check runs inside the DNS resolver too, so the dialed address is the validated one.Target a public address. A host on the local network is reachable through the extension backend, which asks the user for network.fetchLocal.No
429 {"error": "rate limit exceeded"}No error: the Response resolves with ok: falsecloud.fetch, and the root fetch on the cloud pathThe per-caller request-rate budget for this tier is spent.Wait, then retry; the budget refills.Yes, after a wait
429 {"error": "upstream origin is saturated"}No error: the Response resolves with ok: falsecloud.fetch, and the root fetch on the cloud pathThe proxy holds too many concurrent requests to that one upstream origin for this caller.Retry, and cap how many requests the app keeps open against one origin.Yes, immediately in most cases
413 {"error": "request body exceeds POST_MAX_BODY_SIZE"}No error: the Response resolves with ok: falsecloud.fetch, and the root fetch on the cloud pathThe request body is larger than the configured cap.Send a smaller body, or split the upload.No
502 {"error": "upstream fetch failed: <e>"}No error: the Response resolves with ok: falsecloud.fetch, and the root fetch on the cloud pathThe upstream connection or read failed. <e> is the transport error text.Retry. Read <e> for what the upstream did.Yes

A row marked No error describes a call that resolves. Check response.ok and the error field of the body instead. The details are on fetch().

Node’s net, dgram, http and dns fail with these errors, and so do their cloud aliases:

MessageName or codeSurfaced byWhat happenedWhat to doRetryable
FKN WebVPN does not support IPC connectionsErrornet.Socket#connect, net.createConnection, net.Server#listenThe first argument is a string (a unix socket path) or a Node handle. Thrown synchronously by the argument normaliser.Use a port, or a { port, host } object.No
FKN WebVPN does not support file descriptorsErrornew net.Socket({ fd })options.fd is set.Drop fd.No
Socket not connectedErrornet.Socket#_read, #_write (as the write callback's error), #end, #destroy internalsA read, write or teardown ran before connect() armed the socket promise.Call connect() first, or wait for the connect event.No, until connect() is called
Socket is not connectedErrornet.Socket#localAddress, #localPort, #localFamily, #remoteAddress, #remotePort, #remoteFamilyThe endpoints have not landed yet. The getters throw rather than answer undefined.Read them inside the connect handler. socket.address() answers {} instead of throwing.Yes, after connect fires
Method not implemented.Errornet.Server#getConnections(cb)The browser transport has no connection table.Count connection events yourself.No
Cannot set socket option before connectError, emitted as 'error'net.Socket#setKeepAlive, #setNoDelay and friends before connectThe socket promise does not exist yet. Emitted, not thrown.Set options after connect.No
Missing optionsErrornew dgram.Socket()The constructor requires options. The @fkn/dgram shim's type cast hides this, so new Socket() typechecks and throws at run time.Use dgram.createSocket('udp4').No
Socket not boundErrordgram.Socket#close, #connect, #disconnect, #send internalsThe socket was never bound. Thrown synchronously.Call bind() first, or use send(), which binds on its own.No, until bind()
ERR_SOCKET_DGRAM_NOT_CONNECTEDError (the code is the whole message)dgram.Socket#disconnect, #remoteAddress()The socket holds no remote.Call connect(port, address) first.No
EBADFError (the code is the whole message)dgram.Socket#address()No local address yet.Read it inside the listening handler.Yes, after listening
"offset" is outside of buffer bounds, "length" is outside of buffer boundsRangeError, code ERR_BUFFER_OUT_OF_BOUNDSdgram.Socket#send(msg, offset, length, ...)The range form was used with an offset or length outside the payload. Thrown synchronously.Fix the range.No
Invalid multicast address: <address>Error, emitted as 'error'dgram.Socket#addMembership, #dropMembershipThe string parses as neither IPv4 nor IPv6.Pass a literal group address.No
Invalid IPv4 address: <s>, Invalid IPv6 address: <s>Errordgram.Socket#send on the data-port path, #setMulticastInterfaceAn address literal failed to encode into wire bytes.Pass a valid literal.No
Cannot set headers after they are sent to the clientError, code ERR_HTTP_HEADERS_SENThttp OutgoingMessage#setHeaderThe head block already went out.Set headers before the first write.No
Cannot render headers after they are sent to the clientError, code ERR_HTTP_HEADERS_SENThttp ServerResponse#writeHeadwriteHead was called twice, or after the head went out. Node merges instead; this throws.Call writeHead once.No
Cannot remove headers after they are sent to the clientErrorhttp OutgoingMessage#removeHeaderThe head block already went out.Remove headers before the first write.No
Header name must be a valid HTTP token [<name>]TypeError, code ERR_INVALID_HTTP_TOKENhttp OutgoingMessage#setHeaderThe name is not a string or fails the HTTP token pattern.Fix the name.No
Invalid value "undefined" for header "<name>"TypeError, code ERR_HTTP_INVALID_HEADER_VALUEhttp OutgoingMessage#setHeaderThe value is undefined.Pass a value or omit the header.No
Socket is not availableError, passed to the write callbackhttp OutgoingMessage#write, #endThe response is not bound to a socket.Stop writing once the socket is gone.No
@fkn/lib: no broker connection within <ms>ms, so <what> could not be requestedBrokerUnreachableErrornet.Socket#connect (<what> is an outbound tcp socket), net.Server#listen (a tcp listener), dgram.Socket#bind (a udp socket)No broker connection exists within the deadline: 8000 ms the first time, 1000 ms after any miss. In a worker realm this is the normal answer until the page calls relayWorker.Bridge the worker with relayWorker(worker), or wait and retry. See Connection and lifecycle.Yes, once the broker is up
getaddrinfo ENOTFOUND <hostname>Errornet.Socket#connect, net.Server#listen, dgram.Socket#bind, #connect, #send to a hostnameThe broker's own dns.lookup returned nothing for the name. The code, errno, syscall and hostname fields are set in the broker realm and are not guaranteed to survive the hop.Match the message, not the code. Check the name; dns.lookup(hostname) answers the same question directly.Sometimes, DNS can change
tcp connect to <address>:<port> timed out after 12000msErrornet.Socket#connectThe relay did not acknowledge the connect within 12000 ms.Retry. If the session produced no metadata at all the transport is also marked stalled and the next dial picks another relay.Yes
The upstream OS error text, for example Connection refused (os error 111)Errornet.Socket#connectThe relay dialed the target and the kernel refused or reset. The relay's own I/O error string is truncated for the wire and rethrown verbatim in the app realm, so the text is the operating system's; the example is the Linux wording and is illustrative.Handle it as you would Node's ECONNREFUSED, matching the text: no Node code survives the hop.Depends on the peer
webvpn: egress to a non-public address refusedErrornet.Socket#connect, net.Server#listen, dgram.Socket#bind, #connectThe target resolves to a loopback, private, link-local or otherwise non-public address and private-target filtering is on. A loopback target is paired locally first, so this is only reached when no local listener owns the port.Use a public target, or a local net.Server in the same broker data plane for loopback pairing.No
webvpn: socket capacity reachedErrornet.Socket#connect, net.Server#listen, dgram.Socket#bindThe session's per-session socket budget is spent.Close what you no longer need, then retry.Yes, after closing sockets
webvpn: socket control queue saturatedErrornet.Socket#connect, net.Server#listen, dgram.Socket#bindThe session's metadata control queue is full.Back off and retry.Yes
webvpn: account session capacity reachedErrorany socket call that opens a new relay sessionThe account already holds the maximum number of relay sessions.Close another tab or device that is using the relay, then retry.Yes, after closing a session elsewhere
webvpn: token not acceptedErrorany socket call that opens a new relay sessionThe rate token presented at session setup was refused.Reconnect the account; a fresh token is minted in the background.Yes
webvpn: broadcast is not available while private-target filtering is enabledErrordgram.Socket#setBroadcast then #send to a broadcast addressBroadcast is off while the private-target filter is on.Use a unicast target.No
WebVPN setup timeout: <what> took over <ms>msErrornet.Socket#connect, net.Server#listen, dgram.Socket#bindA setup step (for example tcp data stream claim) did not complete inside its deadline.Retry.Yes
WebVPN session closedErrorevery socket, on its 'error' event, and dgram.Socket on transport closeThe relay session ended under the socket. The dgram path substitutes the close reason when the transport supplied one.Reopen the socket. dgram only emits this when an 'error' listener is attached, so 'close' is never swallowed.Yes, a new call redials
FKN WebVPN: no relay reachableErrorthe first socket call in a realmEvery ranked relay failed to dial and none produced a more specific error.Retry after a backoff. A failed relay cools for 60000 ms.Yes
relay directory returned <status>, relay directory answered <content type>, relay directory is empty, no relay directory is configuredErrorthe first socket call in a realmThe relay directory fetch failed, answered the wrong content type, listed nothing, or is unset.Retry the first three; the fourth is a build without a directory and needs a rebuild.Yes for the first three
tcp listener closedErrornet.Server, on pending accept claimsThe listener was closed while a connection claim was outstanding.Ignore it during teardown.No, the server is closed
tcp listener already unbound by relay, tcp socket already shut down by relayErrornet.Server#close, net.Socket#end, #destroyThe relay tore the resource down before the local teardown ran.Ignore it during teardown.No
Invalid packet type <type>, expected <expected>Errorany socket callThe relay answered with a packet the client did not expect, which means the two ends disagree on the wire format.Report it. A reload picks up a newer broker.No

A refused connect, bind or listen reports its error on the socket’s error event. The details are on TCP and UDP sockets and HTTP and DNS.

cloud.fs, the hybrid fs and opfs share these errors, including the messages the storage service itself returns:

MessageName or codeSurfaced byWhat happenedWhat to doRetryable
storage locked: this account stores encrypted data, unlock it from the FKN card or unlock()StorageLockedError, code FKN_E2E_LOCKEDcloud.fs.readFile, readFileSealed, writeFile, and the same members through promisesThe broker holds no usable key for this app. The call raises the unlock card first and waits; this error arrives only after the card is dismissed, when no window can show it, or when the popup delivered no usable key.Call unlock() or point the user at the FKN card, then retry. The message deliberately does not match /not found/: locked is unreadable, not empty. See Encryption.Yes, after unlock()
storage: no object at that path, or the original sentence (Not found, storage: read failed (404))StorageNotFoundError, code FKN_STORAGE_NOT_FOUNDcloud.fs.readFile, readFileSealed, writeFileThere is no object at the path. Absence arrives worded two different ways: the api refusing to presign a path with no committed row, and a presign that succeeded followed by a 404 on the object. Both re-mint to this one class.Test isNotFound(err) or err.code === STORAGE_NOT_FOUND, never the message. "Nothing here" is safe to overwrite; "I could not tell you" is not.No
storage: api unreachableError (a FKN_API_UNREACHABLE code is set in the data plane and does not reliably survive the hop)every cloud.fs memberThe fetch to the api's GraphQL endpoint threw. Distinct from an answered error on purpose: a caller may relax an obligation on "nobody answered", never on an answered 500.Match message.startsWith('storage: api unreachable'). Keep the local copy and retry later.Yes
storage: not connectedErrorevery cloud.fs memberNo connect token for this scope, so the account is not connected to this site.Call connect() or account.login(). See Account and quota.Yes, after connect()
storage: reserved pathErrorcloud.fs reads, writes and deletes on .fkn or .fkn/*That prefix is platform-internal.Use another path.No
storage: read failed (<status>)Errorcloud.fs.readFile, readFileSealedThe presigned object fetch answered a non-2xx other than 404. A 404 becomes StorageNotFoundError instead.Retry.Yes
storage: write failed (<status>)Errorcloud.fs.writeFileThe presigned upload PUT answered a non-2xx.Retry.Yes
storage query failed: <status>Errorevery cloud.fs memberThe GraphQL response was not ok, or carried no data, and no errors array explained it.Retry.Yes
Invalid pathErrorcloud.fs reads, writes, deletes, renameThe path is empty, longer than 1024 characters, has more than 64 segments, holds a control character, or has any empty, . or .. segment. A leading slash produces an empty first segment, so /library/catalog.json is refused.Use a relative path such as library/catalog.json.No
Storage quota exceededErrorcloud.fs.writeFileThe account's byte limit would be passed. Checked at presign and again at commit.Delete something or upgrade. cloud.fs.quota() reports the headroom.No, until space is freed
Object limit exceededErrorcloud.fs.writeFileThe account holds the maximum number of objects and this write would add one more.Delete an object first.No
Object too largeErrorcloud.fs.writeFileThe declared size, or the uploaded object's real size, is over the per-object cap.Split the file.No
Concurrent update, retryErrorcloud.fs.writeFile, unlinkAnother writer moved the row between the presign and the commit, or between reading and deleting. The compare-and-set refused.Retry the whole write.Yes
Upload not foundErrorcloud.fs.writeFileThe commit named an upload key with no object behind it.Retry the write from the start.Yes
Not signed inErrorevery cloud.fs memberThe bearer resolved to no live session, or its credential generation is stale. The data plane also reports a connect refusal when it sees this exact sentence.Call account.login().Yes, after signing in
Storage is not configuredErrorevery cloud.fs memberThe api instance has storage disabled.Report it; the app cannot change this.No
Not foundErrorcloud.fs.unlink and other members on a .fkn-prefixed or absent rowThe row does not exist, or the path starts with the platform prefix. On readFile and writeFile this is re-minted to StorageNotFoundError; unlink does not run the re-mint, so it arrives as a plain Error and isNotFound answers false.For deletes, match the message, or treat any delete failure as best effort.No
<CODE>: <text>, <syscall> '<path>'Error, with code, path and syscallevery node-style member of fs, opfs, cloud.fsThe usual filesystem conditions, for example ENOENT: no such file or directory, stat 'a/b'. Codes in use: ENOENT, EEXIST, ENOTDIR, EISDIR, ENOTEMPTY, ERR_FS_EISDIR, EBUSY, EINVAL.Branch on err.code exactly as with Node's fs.Depends on the code
storage: <path> exists but could not be read, retry once its scope is availableError, code FKN_E2E_LOCKEDfs.readFileSync, statSync, writeFileSync, renameSync, open, stat, rename on an unhydrated pathThe path is known from a listing but its bytes could not be read into the memory copy, typically because the account is locked. Such a path still answers exists() and still appears in readdir.Unlock, then remount(). Deleting it needs no key.Yes, after unlock()
The "data" argument must be of type string, Buffer, TypedArray, or DataViewTypeErrorfs.writeFile, writeFileSync, appendFile, appendFileSync, and the cloud.fs equivalentsA Blob, an object or anything else was passed as the data.Convert to bytes first. Only a conflict resolver may hand back a Blob.No
opfs: invalid pathErrorfs.pull(path), fs.readFileSealed(path), and every direct opfs memberThe path holds a .. segment, or normalises to nothing. The memory layer never sends .., but pull and readFileSealed pass the raw string.Normalise the path before calling.No
fs: cloud unreachableErrorfs.writeFile, remove, adoptThe cloud state probe answered 'unknown', so nobody could be asked, and there is no local half to queue from. Kept distinct from the row below on purpose.Retry. The probe costs up to 8000 ms the first time and 1000 ms after a timeout.Yes
fs: no storage backend availableErrorfs.writeFile, removeNo OPFS in this realm and the account answered a definite sign-out.Sign in, or accept that this realm has no durable store.Yes, after signing in
fs: this needs a signed-in accountErrorfs.adopt()The cloud half answered 'disconnected'.Call account.login() first.Yes, after signing in

Only a read or a write turns the locked and not-found rows into a StorageLockedError or a StorageNotFoundError. A cloud.fs.unlink failure is not converted, so isNotFound returns false for it. The node-style members carry Node’s own error.code, listed on storage.

Encryption has three messages of its own, exported as constants from @fkn/lib/messages. The storage service answers with four more on a write:

MessageName or codeSurfaced byWhat happenedWhat to doRetryable
fkn:e2e-lockedError on the wire, re-minted to StorageLockedError in the app realmcloud.fs reads and writesThe broker holds no usable key for this app. This is the wire form of storage locked, exported as E2E_LOCKED_MESSAGE.Handle StorageLockedError instead; match this prefix only when reading the raw broker error, as a package does.Yes
fkn:e2e-stale-epoch: this file is encrypted under a previous key you resetErrorcloud.fs.readFile, readFileSealedThe object was sealed under a key generation the user has since reset. The old key is gone everywhere. Exported as E2E_STALE_EPOCH_MESSAGE.Do not retry and do not overwrite blindly. The only way back is an encrypted export taken before the reset. See Encryption.No, never
fkn:e2e-integrity: stored data failed its integrity checkErrorcloud.fs.readFile, readFileSealedThe row carries no seal marker, an unknown marker, a scope that disagrees with the bearer, or an envelope whose tag did not verify. Exported as E2E_INTEGRITY_MESSAGE.Never overwrite the path on this error. Surface it and stop.No
Superseded keyErrorcloud.fs.writeFileThe account's key rotated between sealing the bytes and committing them. The commit names the generation it sealed under, so this is caught rather than silently stored.Retry the write; it reseals under the current generation.Yes
This account has no encryption keys yetErrorcloud.fs.writeFileThe account holds no key record, which is what a settled key reset leaves behind.Send the user to the security page to enrol again. cloud.fs.encryption() answers enrolled: false in this state.Yes, after re-enrolling
This account stores sealed objectsErrorcloud.fs.writeFileA write arrived with no seal marker for an account that stores sealed objects.Report it; the broker seals every write, so an app cannot produce this on its own.No
Invalid encryptionErrorcloud.fs.writeFileThe marker on the commit is not one the api recognises.Report it; the marker is set by the broker, not by the app.No

Never retry or overwrite after the stale-epoch row (the file was encrypted under a previous key generation) or the integrity row. The reasons are on encryption.

attachFrame, extension.attachFrame and cloud.attachFrame produce these errors. So do goto and the liveness checks that every later call on the Frame runs:

MessageName or codeSurfaced byWhat happenedWhat to doRetryable
cloud.attachFrame needs a window realmErrorcloud.attachFrame, and attachFrame when it falls to the cloud backendNo window.Attach on the main thread.No
cloud.attachFrame does not support lockdown; use the extension backend for a sealed frameErrorcloud.attachFramelockdown: true was passed. attachFrame routes lockdown to the extension backend before this can fire; only a direct cloud.attachFrame call reaches it.Use extension.attachFrame, or drop lockdown.No
cloud.attachFrame: the iframe must be connected to the document before attachingErrorcloud.attachFrameThe iframe is not in the document.Append the iframe first.Yes, after appending it
cloud.attachFrame: the iframe's sandbox attribute must include <tokens> for the render proxy to loadErrorcloud.attachFrameA sandbox attribute is present without allow-scripts and allow-same-origin. No sandbox attribute at all is fine.Add the tokens or remove the attribute.No
cloud.attachFrame: the iframe src is not a valid URL: <src>Errorcloud.attachFrameThe src attribute is present but does not resolve to a URL.Fix the src, or leave it empty and use goto.No
cloud.attachFrame: this iframe is already attached; navigate with the Frame returned by that attach or use a fresh iframeErrorcloud.attachFrameThe src already points at the render proxy page.Reuse the Frame from the first attach, or use a fresh iframe.No
attachFrame: refusing to target the extension's own pages or an FKN platform originErrorcloud.attachFrame, extension.attachFrame, attachFrameThe target is an extension page or a platform host. Checked page-side for a fast error and again authoritatively in the content script and the render proxy page.Point the frame elsewhere.No
cloud.attachFrame: the iframe window is not availableErrorcloud.attachFramecontentWindow is null right after src was set.Retry with a fresh iframe.Yes
cloud.attachFrame: timed out connecting to the render proxy pageErrorcloud.attachFrameThe handshake with the render proxy page did not connect within 20000 ms. On failure the iframe's referrerPolicy, allow and src are restored.Retry.Yes
cloud.attachFrame: the render proxy never became readyErrorcloud.attachFrameThe render proxy page connected but did not report ready within 65000 ms.Retry.Yes
attachFrame: lockdown needs domains when the frame already has a src; pass domains or load it via gotoErrorextension.attachFrame, attachFrame with lockdownA headers-based lockdown cannot cover a document that is already loading.Pass domains, or attach a blank iframe and goto.No
attachFrame: the iframe must be connected to the document before attachingErrorextension.attachFrame, attachFrameThe iframe is not in the document. Checked after the exposure wait, so a missing extension is reported first.Append the iframe first.Yes
frame: no iframe registered for marker="<marker>"Errorextension.attachFrameThe CustomEvent carrying the attach marker never reached the content script.Retry the attach.Yes
The FKN WebExtension is not installed, enabled or not exposed on this page.Errorextension.attachFrame, attachFrameThe exposure wait runs before the connected-iframe check, so a missing extension is reported first. ExtensionOutdatedError would be the other outcome of the same wait, but cannot fire in this version.Offer promptInstall(reason) or a link to the store listing, as for the fetch row of the same name.Yes, once installed
frame goto: "<url>" is not a url this page can resolveErrorextension frame.gotoThe url does not resolve against the app page's location.href.Pass an absolute url.No
frame load failed for <href>Errorextension frame.goto with the default waitUntil: 'load'The iframe fired error.Retry, or check the target.Yes
frame load timed out after <ms>ms for <href>Errorextension frame.gotoNo load within timeoutMs, default 30000.Raise timeoutMs or retry.Yes
frame goto: documentstart timed outErrorextension frame.goto with waitUntil: 'documentstart'The new document's content script did not announce itself in time.Retry.Yes
cloud.attachFrame: the render proxy did not answer goto; the frame may have been detached or its page reloadedErrorcloud frame.gotoThe render proxy page did not answer within timeoutMs (default 30000) plus 5000 ms.Check the frame is still attached, then retry with a fresh attach.Sometimes
navigation to <url> did not commit; the frame is still blankErrorcloud frame.gotoThe render proxy navigated but no document committed.Retry.Yes
frame load timed out after <ms>ms for <url>Errorcloud frame.gotoThe render proxy's own load deadline passed.Retry.Yes
Permission denied: embed.open (<href>)PermissionDeniedErrorextension frame.gotoThe user refused the navigation.Match error.name === 'PermissionDeniedError' and explain the refusal. See Permissions and consent.Yes
cloud.attachFrame: the attached iframe left the document or was reloaded; attach a fresh iframeLocatorUnsupportedErrorevery locator call and goto on a cloud FrameThe iframe was detached, an ancestor was removed, or contentWindow changed. Only the direct parent is watched; an ancestor removal is caught on the next call. The name is set deliberately so the dispatch loop stops instead of retrying a dead channel.Attach a fresh iframe.No, this attachment is over
frame: this frame no longer holds the document the app attached it toLocatorDeniedError (terminal)every locator call on an extension FrameThe framed document navigated somewhere the app never declared. The message deliberately does not say where it went.goto a declared target again. The policy travels down every frameLocator hop.No
frame: <operation> is only available on an attached frameLocatorDeniedError (terminal)frame.fetch on a frame that is not an attachmentfetch is refused outright on a frame that is not an attachment.Use the Frame returned by attachFrame.No

What each backend (the cloud, the extension or the desktop) refuses is on frames, and how long goto waits is on timeouts.

Every selector and action on a Locator can fail with these errors, and so can the frameLocator and owner calls and frame.fetch:

MessageName or codeSurfaced byWhat happenedWhat to doRetryable
No elements foundLocatorErrorclick, fill, hover, textContent, getAttribute, isVisible, videoElementThe chain resolved to nothing. This is the message a "timeout" usually carries, since the loop rethrows the last attempt.Widen the selector, or raise timeout.Yes, to the deadline
Strict mode violation: locator resolved to <n> elementsLocatorErrorthe same operationsThe chain matched more than one element and the operation needs exactly one.Narrow the selector. count and exists accept any number.Yes, to the deadline
Strict mode violation: expected <name>, got <tag>LocatorErrorvideoElement and other tag-pinned operationsThe single match is the wrong element type.Fix the selector.Yes
Locator timeout (<ms>ms): <operation>Errorany operationThe deadline passed with no attempt having failed first, for example a single attempt that hung.Raise timeout, or find out why the attempt hangs.No
fill: element <tag> is not fillableErrorfillThe element is not an <input>, <textarea> or contenteditable node.Target a real field.Yes, pointlessly
frameLocator: no <iframe> matchedLocatorErrorany call whose chain crosses a frameLocatorThe barrier selector matched nothing.Wait for the iframe, or fix the selector.Yes, to the deadline
frameLocator: expected <iframe>, got <<tag>>LocatorErrorany call whose chain crosses a frameLocatorThe barrier's first match is not an iframe.Fix the selector.Yes
frameLocator: this realm has no bridge into child framesLocatorUnsupportedError, operation 'frameLocator'any call whose chain crosses a frameLocatorThe realm cannot descend.Do not descend from this realm; drive the child from a realm that has a bridge.No
frameLocator: <iframe> has no contentWindowErrorany call whose chain crosses a frameLocatorThe iframe holds no window yet.Wait for it to load.Yes
owner: this realm has no bridge to its parent frameLocatorUnsupportedError, operation 'owner'owner()No upward bridge in this realm.Do not climb from here.No
owner: already at the top frameLocatorUnsupportedError, operation 'owner'owner()The chain is already at the top.Stop climbing.No
owner: the parent frame is outside this bridge boundaryLocatorUnsupportedError, operation 'owner'owner() on the render proxyThe bridge refuses to climb out of the proxied content.Stop climbing.No
the frame this call was routed to went away mid-call, retryingLocatorErrorany call across a frameLocator hopThe child frame navigated mid-call.Let the dispatch loop retry; no handling is needed.Yes, to the deadline
the attached frame became stale during the call, retryingLocatorErrorany call on an extension FrameThe attached frame navigated mid-call.Let the dispatch loop retry; no handling is needed.Yes
frame-locator: the target frame has no window yet, frame-locator: registration <id> went away before it answered, retryingLocatorErrorany call across a window bridgeBridge registration raced the call.Let the dispatch loop retry; no handling is needed.Yes
Unknown locator kind: "<kind>", Unknown selector: "<kind>.<name>"LocatorUnsupportedErrora chain built with a selector this stack does not registerThe selector module is not in the registry. @fkn/lib's Locator is pre-bound to the extension stack's registry, which adds videoElement and the reason option.Use the registered selectors. See Locators and actions.No
Locator operation not supported on the <backend> backend: <operation>LocatorUnsupportedError, with operation and backendany operation a backend refusesThe backend does not serve that operation.Use the other backend.No
requestPictureInPicture is not supported in this environment, exitPictureInPicture is not supported in this environmentErrorvideoElement().requestPictureInPicture(), .exitPictureInPicture()The framed document's browser lacks the method.Feature-detect first.No
videoElement: unknown method "<method>"Errora videoElement handle method that does not existOnly play, pause, load, requestPictureInPicture and exitPictureInPicture are callable.Call one of the five methods that exist.No
Index or size is negative or greater than the allowed amountDOMException, name IndexSizeErrorvideoElement state's buffered.start(i), .end(i), seekable.*The index is out of range on the rebuilt TimeRanges.Check length first.No
fetch: url must be a stringLocatorInvalidError, operation 'fetch'frame.fetch on either backendThe url argument is not a string.Pass a string.No
fetch: not a valid url: <url>LocatorInvalidError, operation 'fetch'frame.fetchThe url does not resolve against the landing document's baseURI.Pass a valid url.No
frame.fetch on the cloud backend needs its own session: attach with syncCookies: falseLocatorDeniedError (terminal)cloud frame.fetchThe attachment uses the shared render proxy jar, which is one record shared by every app. No grant can lift this.Re-attach with syncCookies: false.No
frame.fetch: url must be a stringLocatorDeniedError (terminal)cloud frame.fetch, and cloud ensure('fetch')The page-side gate sees a non-string where the url should be. ensure('fetch') hits this because the gate reads the options object as the url.Pass a url, or skip ensure('fetch') on the cloud backend.No
frame.fetch on the cloud backend needs an absolute urlLocatorDeniedError (terminal)cloud frame.fetchA relative url resolves in the landing realm, which the app page cannot know.Pass an absolute url.No
frame.fetch: only http(s) urls are supportedLocatorDeniedError (terminal)cloud frame.fetchThe scheme is neither http nor https.Use http or https.No
frame.fetch: the target is outside the origins this attachment declaredLocatorDeniedError (terminal)cloud frame.fetchThe hostname is not in domains and the origin is neither the attach target nor a goto target. The message deliberately does not echo where the call tried to go.Declare the host in domains at attach, or goto it first.No
frame.fetch: the user did not grant thisLocatorDeniedError (terminal)cloud frame.fetchThe consent card was refused or dismissed. A dismissal starts a 10000 ms cooldown during which the card is not shown again and the call fails closed.Ask again after the cooldown, or explain why the app needs it.Yes, after the cooldown and a grant
frame.fetch: the user has not granted thisLocatorDeniedError (terminal)cloud frame.fetchThe render proxy page re-ran the same policy and found no stored grant, without prompting.Ask again later, or explain why the app needs it.Yes
frame.fetch: the call completed but its audit receipt could not be recorded; result withheldLocatorDeniedError (terminal)extension frame.fetchThe fetch ran but its activity-log receipt could not be written. The result is withheld rather than returned unrecorded, and the name is terminal so the loop does not re-issue a state-changing request.Treat the request as having happened with an unknown result.No
fetch on the shared render proxy session, fetch on a frame holding no proxied document; navigate first, fetch is not available while the proxied document is on its own originLocatorUnsupportedError, backend 'render proxy'cloud frame.fetchThe render proxy's own refusals. The third fires whenever the proxied document sits on its own frame-host origin, which is the deployed default, so cloud frame.fetch normally ends here.Use the extension backend for frame fetches.No

A timeout rarely says so. The retry loop runs until its 30,000 ms deadline and then throws the error from the last attempt. isTerminalError matches the three error names that stop the loop early, listed on locators and actions.

Every gated extension call can fail with the consent refusal. It is the error your call gets when a user denies a row on the consent sheet, the prompt the extension shows before an action above severity 0. The permission channel has two errors of its own:

MessageName or codeSurfaced byWhat happenedWhat to doRetryable
Permission denied: <key>, Permission denied: <key> (<scope>)PermissionDeniedError, with permissionKey and scopeevery gated extension call: fetch with credentials: 'include', cookies.get, setRequestHeaderRule, every locator operation, frame.goto, frame.fetch, extension.attachFrameThe user refused, dismissed the consent sheet, or a stored deny covers the scope. A dismissal records deny with remember: 'once', so a retry asks again. The gate raises it before the retry loop, so it is not retried.Match error.name === 'PermissionDeniedError'. isLocatorDenied and isTerminalError do not match this name, which is the single most common wrong assumption in this area. See Permissions and consent.Yes if the user changes their mind; no while a session or always deny stands
permission rpc: the background answered with an unknown shapeErrorpermissions.request and anything that consults the storeThe background's reply did not match the expected envelope.Retry; reload the extension if it persists.Yes
Whatever response.error saysErrorpermissions.request and anything that consults the storeThe background answered a structured failure. Its store mints no sentence of its own, so the text is the message of whatever rejected under it, which is the extension's database layer in the browser; nothing in the library fixes it.Read the text.Depends

Neither locator guard matches Permission denied: <key> (<scope>). Compare error.name instead, as the handler above does. The full flow is on permissions and consent.

Every packages.* member, in the host app and in the package it loads, fails with a plain Error that carries one of six codes. @fkn/lib/packages types that shape as PackagesError:

MessageName or codeSurfaced byWhat happenedWhat to doRetryable
packages: no FKN transport in this realm - use relayWorker to bridge workersError, code unavailableevery packages.* member except attach, onConnect, isVisible, onVisibilityChangeNeither window nor self is available.Call relayWorker(worker) from the page. See Workers.Yes, after bridging
packages.<call>: caller identity is not establishedError, code unavailablepick, install, uninstall, connect, mount, show, hideThe broker could not attribute the call to an app identity.Retry after the broker connection settles.Yes
packages.<call>: <uri parse failure>Error, code invalidinstall, uninstall, connect, mount, show, hideThe uri failed to parse. Underlying sentences: A source uri must be a string of 1 to 512 characters, '<input>' has no '<handler>:' prefix, No handler for '<handler>:', and packages: '<handler>:' is not served by the npm registry.Pass npm:<name>.No
packages.install: version must be a valid npm version stringError, code invalidinstallThe pinned version fails the version pattern.Fix the version.No
packages.install: could not persist the install record (storage may be full or blocked)Error, code unavailableinstallThe broker could not write the install record.Free space, or check that storage is not blocked for the origin.Yes
packages.search: query must be an object, packages.search: type must be a short lowercase token, packages.search: id must be a short lowercase token, packages.search: unknown origin '<origin>'Error, code invalidsearch, pickThe query is malformed.Fix the query.No
packages.search: the npm registry did not answerError, code unavailablesearch, pickThe registry request failed.Retry.Yes
packages: could not resolve '<name>' from the npm registryError, code unavailableinstall, pickThe packument fetch failed or answered a non-object.Retry.Yes
packages: '<name>' has no latest version, packages: '<name>' has no version '<version>'Error, code invalidinstall, pickThe registry knows the package but not that version.Pick a published version.No
packages: another package prompt is already openError, code unavailablepick, installThe broker's exclusive UI slot is taken.Wait for the open prompt to close, then retry.Yes
packages.<call>: '<uri>' is not installed by this appError, code not-installedconnect, mountThis app holds no install record for the package.Call packages.install(uri) first.Yes, after install
packages.show: '<id>' is not connected by this app - connect() before showing itError, code not-installedshowThe package is installed but not connected.Call connect(uri) first.Yes
packages.<call>: '<uri>' has been disabled by the platformError, code deniedconnect, mountThe platform has switched the package off.Remove the package from the app; nothing app-side can lift this.No
packages.show: a package cannot place its own frameError, code deniedshow called from inside a package tenantOnly a host app may place a frame.Place the frame from the host app, never from inside the package.No
packages.<call>: '<pinned uri>' does not fit a sandbox origin - use a shorter package nameError, code unaddressableconnect, mountThe version-pinned identity does not encode into a sandbox origin label.Publish the package under a shorter name.No
packages.connect: '<uri>' was released while connectingError, code not-installedconnectThe package was uninstalled mid-connect.Reinstall and retry.Yes
packages.connect: '<uri>' failed to boot: <failure>Error, code unavailableconnectThe tenant frame reported a boot failure.Read <failure>; it is the package's own boot error.Sometimes
packages.connect: '<uri>' did not register a connection handlerError, code timeoutconnectThe tenant never called onConnect.Add a packages.onConnect handler in the package. See Packages.Sometimes
packages.connect: the package did not complete the connectionError, code timeoutconnect, mount, attachThe connection handshake on the port did not settle within 30000 ms.Retry.Yes
packages.connect: the package refused the connection, or the package's own nack textError, code unavailableconnect, mount, attachThe package's createPayload threw, or it sent a nack. The package's Error.message is used when it supplied one.Read the text; it is the package's.Depends
packages.connect: aborted before connectingError, code unavailableconnect, mount, attach with a signalThe signal aborted before or during the handshake.Treat it as the normal result of aborting.Yes
packages.connect: the package closed before connectingError, code unavailableconnect, mountThe broker's closed promise settled during the handshake.Retry.Yes
packages.show: pass an element or a rectError, code invalidshowNeither placement option was given.Pass one.No
packages.show: a rect with finite x, y, width and height is requiredError, code invalidshowThe rect has a non-finite member.Fix the rect.No
packages.mount: pass the iframe to load the package intoError, code invalidmountoptions.iframe is not an HTMLIFrameElement.Pass one.No
packages.mount: the iframe must be in the document before mounting into itError, code invalidmountA detached frame never navigates, so src would resolve into a 30 s wait for a tenant that is not booting.Append the iframe first.Yes, after appending
packages.mount: the iframe's sandbox attribute must include <tokens>, or the package cannot startError, code invalidmountA sandbox attribute without allow-scripts and allow-same-origin. The tenant boots a service worker on its own origin and needs both.Add the tokens or remove the attribute.No
packages.mount: this page is cross-origin isolated, so the iframe's allow attribute must include 'cross-origin-isolated' to hand that down to the packageError, code invalidmount from a cross-origin isolated pageIsolation defaults to self, so a page that does not hand it down silently drops the package to no SharedArrayBuffer.Add cross-origin-isolated to allow before mounting.No
packages.mount: the package did not register a connection handlerError, code timeoutmountThe tenant did not report ready within 30000 ms. The frame is unmounted before this throws.Add a packages.onConnect handler in the package. See Packages.Sometimes
packages.mount: the package failed to boot, or the tenant's own failure textError, code unavailablemountThe tenant reported a boot failure.Read the text; it is the package's.Depends
packages.mount: the frame was detached before it could connectError, code unavailablemountcontentWindow went away between the ready message and the port handoff.Retry with a stable iframe.Yes

Branch on code. A worker that was never relayed has to be relayed from the page first. The codes are on packages and the worker case is on workers.

Every member of @fkn/lib/rooms, and every method on a Room, fails with a plain Error that carries one of ten codes, typed as RoomsError:

MessageName or codeSurfaced byWhat happenedWhat to doRetryable
rooms: no such roomError, code not-foundrooms.joinNo room holds that uuid, and no snapshot of one is fresh enough to restore.Ask for a current invite. A room ends when its last member leaves.No
rooms: wrong room keyError, code bad-keyrooms.joinThe key half of the invite is not the key the room was created with.Pass room.invite whole, <uuid>.<key>, rather than the uuid alone.No
rooms: the invite carries no keyError, code invalidrooms.joinThe invite has no dot, or nothing on one side of it, so @fkn/lib refuses it before the broker is asked.Pass room.invite, or an id and a key joined by a dot.No
rooms: no such memberError, code not-foundroom.grant, room.revoke, room.remove, room.block, room.unblockThe id names nobody this room holds.Take ids from room.members() and from the room events, and drop one on a left event.No
rooms: the room is fullError, code fullrooms.joinThe room already holds its member cap.Wait for a member to leave, or create the room with a larger members.Yes
rooms: too many roomsError, code fullrooms.createThis account, or this address without one, holds its cap of open rooms.Leave a room before opening another.Yes, once a room ends
rooms: too many connectionsError, code fullrooms.create, rooms.joinThe connection cap for this member or this address is reached.Leave a room this browser still holds, then join again.Yes
rooms: too many blocksError, code fullroom.blockThe room's block set is full, and it refuses rather than dropping an entry.Unblock somebody first, since dropping an entry would let them back in.Yes, after an unblock
rooms: you are blocked from this roomError, code blockedrooms.joinA member holding block blocked this caller, and a block lives as long as the room.Nothing app-side lifts it. A member holding block can call room.unblock(id).No
rooms: you cannot send hereError, code deniedroom.sendThis member's send is false, from the room default or from a revoke.Read room.self.permissions.send and hide the composer while it is false.Yes, after a grant
rooms: permission deniedError, code deniedroom.remove, room.block, room.unblockThe caller holds neither remove nor block, whichever the call needed.Offer the control only while room.self.permissions carries the one it uses.Yes, after a grant
rooms: not the room ownerError, code deniedroom.setDefault, room.grant, room.revokeOnly the owner sets defaults and moves permissions.Compare room.self.id with room.owner before offering these three.No
rooms: the owner cannot be removedError, code deniedroom.remove, room.blockThe target is room.owner, who stays for the life of the room.Render no remove or block control against the owner.No
rooms: the owner keeps every permissionError, code deniedroom.grant, room.revokeThe target is room.owner, whose four permissions are constant.Skip the owner when you render per-member permission controls.No
rooms: you cannot remove yourselfError, code deniedroom.remove, room.blockThe target id is room.self.id.Call room.leave(), which is the call that means leaving.No
rooms: sending too fastError, code rate-limitedroom.sendThe per-member or per-room rate is spent, or the platform shed the message before it took a seq.Queue the text and send it again a moment later, and never in a tight loop.Yes
rooms: the message is too largeError, code too-largeroom.sendThe text is over 4,096 bytes of UTF-8, or the sealed frame is over the wire cap. Nothing is trimmed.Split the text and measure its UTF-8 byte length, not its character count.Yes, with shorter text
rooms: rooms are unavailableError, code unavailablerooms.create, rooms.join, and every member of a joined RoomThere is no broker to ask, the broker was replaced while the call was pending, or the platform could not answer the handshake.Check rooms.available() first, and open the room again from the invite.Yes
rooms: rooms are not availableError, code unavailablerooms.create, rooms.joinRooms are switched off on the platform, so every call is refused for now.Branch on rooms.available() and offer the rest of your app without a room.Sometimes
rooms: already joinedError, code invalidrooms.create, rooms.joinA second create or join reached one connection, which carries one membership.Hold the Room the first call resolved and pass it around.No
rooms: malformed frameError, code invalidrooms.create, rooms.join, and every member of a joined RoomThe platform could not read the frame, or a key or seed was not 32 bytes in canonical base64url.Report it. An app calling the documented surface cannot produce this.No
rooms: the room has endedError, code closedevery member of a joined Room, once it has ended for this appThe room is over here: you left, you were removed or blocked, or the rejoin window passed.Read await room.closed for the reason, and call rooms.join(invite) for a fresh Room.No, this Room is finished

Branch on code, never on the text: the wordings are the library’s own literals, kept here so the catalogue can find them, and a code survives a rewording. Which code each method raises is on rooms.

The bounded wait for a broker, a broker replaced while a call was pending, and the two rejections of an awaited relayWorker call all land here:

MessageName or codeSurfaced byWhat happenedWhat to doRetryable
@fkn/lib: no broker connection within <ms>ms, so <what> could not be requestedBrokerUnreachableErrornet.Socket#connect, net.Server#listen, dgram.Socket#bind, and anything else built on apiWithin from @fkn/lib/apiNo broker connection settled within the deadline: 8000 ms, and once any call has missed it, every later call waits only 1000 ms.Use apiWithin(what) wherever the caller owns a socket, a timer or a UI: it is the bounded alternative to apiPromise, which never rejects and parks forever in a realm with no broker. See Connection and lifecycle.Yes
FKN: the broker was replaced while this call was pending; retry itErrorany call in flight when the broker document is replaced (its update flow reloads the broker frame)A call is bound to the broker connection it started on. When that document is replaced, pending calls are rejected rather than left to hang silently; the new connection is already routed.Match message.startsWith('FKN: the broker was replaced') and retry once.Yes, and the message says so
FKN @fkn/lib: relayWorker must be called from the main threadErrorrelayWorkerNo window.Call it from the page.No
FKN @fkn/lib: relayWorker found no FKN transport in this realmErrorrelayWorkerThis realm has neither a mounted broker frame nor a MessagePort granted by a parent FKN realm.Import the library on a page that mounts the broker frame before calling relayWorker.Yes, once the transport is up

connection and lifecycle lists the calls that wait with no deadline of their own.

@fkn/lib 0.9.28 declares four errors that no routed call reaches. They are listed here so that you do not write a handler for them:

MessageName or codeWhy it cannot fire
The FKN WebExtension is installed but too old for this page: it speaks ABI <abi> and this page needs at least <required>. Updating the extension fixes this.ExtensionOutdatedErrorIt is thrown only when the extension ABI is below the required one, and the required ABI is 0 in this release while every parsed value is clamped to 0 or more, so the comparison is never true.
The FKN WebExtension does not support "<operation>" (ABI <abi>). Updating the extension may add it.ExtensionOperationUnsupportedErrorThe class is exported but nothing in the library constructs it; supportsOperation is exported for callers to branch on and no code path throws on its false.
Locator operation not supported on the cloud backend: <operation>LocatorUnsupportedErrorThe cloud backend's set of unsupported operations is empty, so the guard before every call never matches, and the render proxy registers videoElement as well, so nothing refuses it either.
The FKN desktop app is not connected, desktop.<name> is unavailableErrorNo automatic routing reaches the desktop backend: desktop.available() is hardcoded false, so the root fetch never falls to it, and only a direct desktop.* call sees the throw.

The two classes are still exported, so a catch that names them type-checks and never runs. Only a direct desktop.* call sees the desktop row, as backends explains.