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:
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(
constE2E_STALE_EPOCH_MESSAGE:"fkn:e2e-stale-epoch: this file is encrypted under a previous key you reset"
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(
constE2E_INTEGRITY_MESSAGE:"fkn:e2e-integrity: stored data failed its integrity check"
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.
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
constretryOnce: () =>Promise<string|Buffer>
retryOnce()
// locators and frames, by name, through the exported guards
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 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 domains
Error
extension.fetch, and the root fetch whenever the extension backend is chosen
The 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 realms
Error
root 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.
Error
extension.fetch, root fetch with credentials: 'include', extension.attachFrame, attachFrame, cookies.get, permissions.request, setRequestHeaderRule, removeRequestHeaderRule: everything that goes through the extension bridge
The 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>
Error
extension.fetch
The 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
fetch: a forged Cookie header and credentials:'include' are different identities - send one or the other
Error
extension.fetch
Both a non-empty forged cookie header and credentials: 'include' were passed.
Pick one identity and drop the other. There is deliberately no precedence rule.
No
fetch: could not install the header rule for a forged Cookie, so the request was not sent unauthenticated
Error
extension.fetch with a forged cookie
The session rule that carries the cookie could not be installed, for example because the url is too long for the rule filter. The request is not sent rather than sent without the cookie.
Shorten the url or drop the forged cookie. A failed rule for origin or referer is not fatal: that request runs without the header.
Sometimes, with a shorter url
FKN cloud.fetch: no proxy is available (the relay directory could not be read, and no fallback origin is configured)
Error
cloud.fetch, root fetch on the cloud path
The 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 document
Error
removeRequestHeaderRule(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 TypeError
TypeError
extension.fetch
The 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.
PermissionDeniedError, with permissionKey and scope
extension.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: false
cloud.fetch, and the root fetch on the cloud path
The 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: false
cloud.fetch, and the root fetch on the cloud path
No 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: false
cloud.fetch, and the root fetch on the cloud path
The 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.
Every 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: false
cloud.fetch, and the root fetch on the cloud path
The 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: false
cloud.fetch, and the root fetch on the cloud path
The 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: false
cloud.fetch, and the root fetch on the cloud path
The 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: false
cloud.fetch, and the root fetch on the cloud path
The 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().
dgram.Socket#send on the data-port path, #setMulticastInterface
An address literal failed to encode into wire bytes.
Pass a valid literal.
No
Cannot set headers after they are sent to the client
Error, code ERR_HTTP_HEADERS_SENT
httpOutgoingMessage#setHeader
The head block already went out.
Set headers before the first write.
No
Cannot render headers after they are sent to the client
Error, code ERR_HTTP_HEADERS_SENT
httpServerResponse#writeHead
writeHead 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 client
Error
httpOutgoingMessage#removeHeader
The 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_TOKEN
httpOutgoingMessage#setHeader
The 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_VALUE
httpOutgoingMessage#setHeader
The value is undefined.
Pass a value or omit the header.
No
Socket is not available
Error, passed to the write callback
httpOutgoingMessage#write, #end
The 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 requested
BrokerUnreachableError
net.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.
net.Socket#connect, net.Server#listen, dgram.Socket#bind, #connect, #send to a hostname
The 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 12000ms
Error
net.Socket#connect
The 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)
Error
net.Socket#connect
The 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.
The 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.
cloud.fs, the hybrid fs and opfs share these errors, including the messages the storage service itself returns:
Message
Name or code
Surfaced by
What happened
What to do
Retryable
storage locked: this account stores encrypted data, unlock it from the FKN card or unlock()
StorageLockedError, code FKN_E2E_LOCKED
cloud.fs.readFile, readFileSealed, writeFile, and the same members through promises
The 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_FOUND
cloud.fs.readFile, readFileSealed, writeFile
There 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 unreachable
Error (a FKN_API_UNREACHABLE code is set in the data plane and does not reliably survive the hop)
every cloud.fs member
The 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 connected
Error
every cloud.fs member
No connect token for this scope, so the account is not connected to this site.
cloud.fs reads, writes and deletes on .fkn or .fkn/*
That prefix is platform-internal.
Use another path.
No
storage: read failed (<status>)
Error
cloud.fs.readFile, readFileSealed
The presigned object fetch answered a non-2xx other than 404. A 404 becomes StorageNotFoundError instead.
Retry.
Yes
storage: write failed (<status>)
Error
cloud.fs.writeFile
The presigned upload PUT answered a non-2xx.
Retry.
Yes
storage query failed: <status>
Error
every cloud.fs member
The GraphQL response was not ok, or carried no data, and no errors array explained it.
Retry.
Yes
Invalid path
Error
cloud.fs reads, writes, deletes, rename
The 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 exceeded
Error
cloud.fs.writeFile
The 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 exceeded
Error
cloud.fs.writeFile
The account holds the maximum number of objects and this write would add one more.
Delete an object first.
No
Object too large
Error
cloud.fs.writeFile
The declared size, or the uploaded object's real size, is over the per-object cap.
Split the file.
No
Concurrent update, retry
Error
cloud.fs.writeFile, unlink
Another 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 found
Error
cloud.fs.writeFile
The commit named an upload key with no object behind it.
Retry the write from the start.
Yes
Not signed in
Error
every cloud.fs member
The 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 configured
Error
every cloud.fs member
The api instance has storage disabled.
Report it; the app cannot change this.
No
Not found
Error
cloud.fs.unlink and other members on a .fkn-prefixed or absent row
The 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 syscall
every node-style member of fs, opfs, cloud.fs
The 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 available
Error, code FKN_E2E_LOCKED
fs.readFileSync, statSync, writeFileSync, renameSync, open, stat, rename on an unhydrated path
The 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 DataView
TypeError
fs.writeFile, writeFileSync, appendFile, appendFileSync, and the cloud.fs equivalents
A 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 path
Error
fs.pull(path), fs.readFileSealed(path), and every direct opfs member
The 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 unreachable
Error
fs.writeFile, remove, adopt
The 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 available
Error
fs.writeFile, remove
No 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 account
Error
fs.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:
Message
Name or code
Surfaced by
What happened
What to do
Retryable
fkn:e2e-locked
Error on the wire, re-minted to StorageLockedError in the app realm
cloud.fs reads and writes
The 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 reset
Error
cloud.fs.readFile, readFileSealed
The 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 check
Error
cloud.fs.readFile, readFileSealed
The 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 key
Error
cloud.fs.writeFile
The 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 yet
Error
cloud.fs.writeFile
The 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 objects
Error
cloud.fs.writeFile
A 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 encryption
Error
cloud.fs.writeFile
The 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.
The 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 available
Error
cloud.attachFrame
contentWindow is null right after src was set.
Retry with a fresh iframe.
Yes
cloud.attachFrame: timed out connecting to the render proxy page
Error
cloud.attachFrame
The 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 ready
Error
cloud.attachFrame
The 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 goto
Error
extension.attachFrame, attachFrame with lockdown
A 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 attaching
Error
extension.attachFrame, attachFrame
The 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>"
Error
extension.attachFrame
The 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.
Error
extension.attachFrame, attachFrame
The 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 resolve
Error
extension frame.goto
The url does not resolve against the app page's location.href.
Pass an absolute url.
No
frame load failed for <href>
Error
extension 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>
Error
extension frame.goto
No load within timeoutMs, default 30000.
Raise timeoutMs or retry.
Yes
frame goto: documentstart timed out
Error
extension 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 reloaded
Error
cloud frame.goto
The 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 blank
Error
cloud frame.goto
The render proxy navigated but no document committed.
Retry.
Yes
frame load timed out after <ms>ms for <url>
Error
cloud frame.goto
The render proxy's own load deadline passed.
Retry.
Yes
Permission denied: embed.open (<href>)
PermissionDeniedError
extension frame.goto
The 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 iframe
LocatorUnsupportedError
every locator call and goto on a cloud Frame
The 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 to
LocatorDeniedError (terminal)
every locator call on an extension Frame
The 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 frame
LocatorDeniedError (terminal)
frame.fetch on a frame that is not an attachment
fetch 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.
a chain built with a selector this stack does not register
The 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.
The index is out of range on the rebuilt TimeRanges.
Check length first.
No
fetch: url must be a string
LocatorInvalidError, operation 'fetch'
frame.fetch on either backend
The url argument is not a string.
Pass a string.
No
fetch: not a valid url: <url>
LocatorInvalidError, operation 'fetch'
frame.fetch
The 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: false
LocatorDeniedError (terminal)
cloud frame.fetch
The 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 string
LocatorDeniedError (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 url
LocatorDeniedError (terminal)
cloud frame.fetch
A 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 supported
LocatorDeniedError (terminal)
cloud frame.fetch
The scheme is neither http nor https.
Use http or https.
No
frame.fetch: the target is outside the origins this attachment declared
LocatorDeniedError (terminal)
cloud frame.fetch
The 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 this
LocatorDeniedError (terminal)
cloud frame.fetch
The 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 this
LocatorDeniedError (terminal)
cloud frame.fetch
The 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 withheld
LocatorDeniedError (terminal)
extension frame.fetch
The 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 origin
LocatorUnsupportedError, backend 'render proxy'
cloud frame.fetch
The 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
navigation pending; the target document has not committed yet
Error (retryable)
any cloud locator call right after a waitUntil: 'documentstart' goto
The render proxy resolved the goto early and the document has not committed.
Let the dispatch loop retry; no handling is needed.
Yes, to the deadline
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:
PermissionDeniedError, with permissionKey and scope
every gated extension call: fetch with credentials: 'include', cookies.get, setRequestHeaderRule, every locator operation, frame.goto, frame.fetch, extension.attachFrame
The 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 shape
Error
permissions.request and anything that consults the store
The background's reply did not match the expected envelope.
Retry; reload the extension if it persists.
Yes
Whatever response.error says
Error
permissions.request and anything that consults the store
The 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.
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:
Message
Name or code
Surfaced by
What happened
What to do
Retryable
packages: no FKN transport in this realm - use relayWorker to bridge workers
Error, code unavailable
every packages.* member except attach, onConnect, isVisible, onVisibilityChange
Neither window nor self is available.
Call relayWorker(worker) from the page. See Workers.
Yes, after bridging
packages.<call>: caller identity is not established
The broker could not attribute the call to an app identity.
Retry after the broker connection settles.
Yes
packages.<call>: <uri parse failure>
Error, code invalid
install, uninstall, connect, mount, show, hide
The 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 string
Error, code invalid
install
The 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 unavailable
install
The 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 invalid
search, pick
The query is malformed.
Fix the query.
No
packages.search: the npm registry did not answer
Error, code unavailable
search, pick
The registry request failed.
Retry.
Yes
packages: could not resolve '<name>' from the npm registry
Error, code unavailable
install, pick
The packument fetch failed or answered a non-object.
Retry.
Yes
packages: '<name>' has no latest version, packages: '<name>' has no version '<version>'
Error, code invalid
install, pick
The registry knows the package but not that version.
Pick a published version.
No
packages: another package prompt is already open
Error, code unavailable
pick, install
The 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 app
Error, code not-installed
connect, mount
This 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 it
Error, code not-installed
show
The package is installed but not connected.
Call connect(uri) first.
Yes
packages.<call>: '<uri>' has been disabled by the platform
Error, code denied
connect, mount
The 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 frame
Error, code denied
show called from inside a package tenant
Only 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 name
Error, code unaddressable
connect, mount
The 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 connecting
Error, code not-installed
connect
The package was uninstalled mid-connect.
Reinstall and retry.
Yes
packages.connect: '<uri>' failed to boot: <failure>
Error, code unavailable
connect
The 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 handler
Error, code timeout
connect
The tenant never called onConnect.
Add a packages.onConnect handler in the package. See Packages.
Sometimes
packages.connect: the package did not complete the connection
Error, code timeout
connect, mount, attach
The 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 text
Error, code unavailable
connect, mount, attach
The 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 connecting
Error, code unavailable
connect, mount, attach with a signal
The signal aborted before or during the handshake.
Treat it as the normal result of aborting.
Yes
packages.connect: the package closed before connecting
Error, code unavailable
connect, mount
The broker's closed promise settled during the handshake.
Retry.
Yes
packages.show: pass an element or a rect
Error, code invalid
show
Neither placement option was given.
Pass one.
No
packages.show: a rect with finite x, y, width and height is required
Error, code invalid
show
The rect has a non-finite member.
Fix the rect.
No
packages.mount: pass the iframe to load the package into
Error, code invalid
mount
options.iframe is not an HTMLIFrameElement.
Pass one.
No
packages.mount: the iframe must be in the document before mounting into it
Error, code invalid
mount
A 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 start
Error, code invalid
mount
A 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 package
Error, code invalid
mount from a cross-origin isolated page
Isolation 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 handler
Error, code timeout
mount
The 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 text
Error, code unavailable
mount
The tenant reported a boot failure.
Read the text; it is the package's.
Depends
packages.mount: the frame was detached before it could connect
Error, code unavailable
mount
contentWindow 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.
Split the text and measure its UTF-8 byte length, not its character count.
Yes, with shorter text
rooms: rooms are unavailable
Error, code unavailable
rooms.create, rooms.join, and every member of a joined Room
There 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 available
Error, code unavailable
rooms.create, rooms.join
Rooms 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 joined
Error, code invalid
rooms.create, rooms.join
A 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 frame
Error, code invalid
rooms.create, rooms.join, and every member of a joined Room
The 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 ended
Error, code closed
every member of a joined Room, once it has ended for this app
The 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:
Message
Name or code
Surfaced by
What happened
What to do
Retryable
@fkn/lib: no broker connection within <ms>ms, so <what> could not be requested
BrokerUnreachableError
net.Socket#connect, net.Server#listen, dgram.Socket#bind, and anything else built on apiWithin from @fkn/lib/api
No 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 it
Error
any 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 thread
Error
relayWorker
No window.
Call it from the page.
No
FKN @fkn/lib: relayWorker found no FKN transport in this realm
Error
relayWorker
This 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.
@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:
Message
Name or code
Why 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.
ExtensionOutdatedError
It 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.
ExtensionOperationUnsupportedError
The 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>
LocatorUnsupportedError
The 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 unavailable
Error
No 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.