The broker is the connection your page holds into FKN. A call that reaches it is answered in another JavaScript context, so the error you catch is only what survived the trip back. This page covers which fields of an error survive, which two errors keep a class, how to test the rest, what a call the extension never finished rejects with, and which failures are worth retrying.
The broker runs in another realm. A realm is one JavaScript execution context, such as a window or a worker. The library mounts a hidden fkn.app iframe, the broker frame, and behind that frame runs a shared worker, the data plane, where the call is served.
An error thrown in the data plane crosses two hops before you catch it: one into the broker frame and one into your realm. Each hop carries name, message, stack and cause and nothing else. The hops are described under how it works.
A read while the storage api is not answering shows what arrives:
name// 'Error', so there is no class to test, only the sentence
constmessage:string
message// 'storage: api unreachable', the prefix you match
constcode:string|undefined
code// undefined, set beside that sentence in the data plane and gone by here
}
Three things follow from that:
instanceof does not work across a hop. The class the broker threw is not a constructor in your realm.
A custom code does not arrive. The data plane sets one beside storage: api unreachable, and only the message prefix reaches you.
name survives. The FKN browser extension and the locators identify their refusals by it.
An error created in your own realm keeps every field. BrokerUnreachableError is a class, so you can test it with instanceof. The library throws it when no broker connection settles within the 8,000 ms broker deadline, or within 1,000 ms once any call in the realm has already missed that deadline.
The ENOENT family on the node-style members of fs, opfs and cloud.fs carries a real code. So does PackagesError, which is a type rather than a class, so its code is the field to test. Both are listed under TypeScript. opfs never reaches the broker, so its errors are ordinary same-realm errors, as described under entry points.
StorageLockedError and StorageNotFoundError are the two exceptions. The library catches every cloud.fs read and write rejection on your side and, for those two, throws a fresh error of its own class with a code. That step is the re-mint.
StorageLockedError replaces an error whose message carries the broker’s fkn:e2e-locked prefix. StorageNotFoundError replaces an error whose code already reads FKN_STORAGE_NOT_FOUND, or, failing that, one whose message reads Not found or ends in (404).
A missing file is reported in two wordings. When the api refuses to presign a path that has no committed row, the message is Not found. When the presign succeeds and the object itself answers 404, the message is storage: read failed (404).
An app that matched only the first wording treated a missing backup as a transient failure and retried forever. isNotFound covers both:
Test in the order the errors were created. The two classes come first, then the message prefixes, then the names, and last the one class that never crossed a hop:
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() // routed to the new broker already
// locators and frames, by name, through the guards
The two classes come first because they carry the only code a cloud.fs rejection can be trusted to have. The prefix branch compares sentences because the E2E_* constants from @fkn/lib/messages and the two fixed prefixes are exactly what the broker sends. The constants are described under encryption, and every message has a row on every error.
The consent sheet is the prompt the extension shows before an action that needs the user’s approval. When the user refuses a row there, the extension rejects with Permission denied: <category> on <site> (<key> <scope>) under the name PermissionDeniedError. Neither locator guard looks for that name:
error) // true, the one guard that reads this name
constname:string
name// 'PermissionDeniedError'
constmessage:string
message// 'Permission denied: interaction on example.org (act.click #play)'
}
The refusal is still terminal. The consent sheet raises it before the retry loop starts, but only the name test above tells you so. The message names the row the user answered first and the concrete call in brackets, and both are described under permissions and consent.
@fkn/lib re-exports the constants LOCATOR_DENIED, LOCATOR_ERROR, LOCATOR_INVALID and LOCATOR_UNSUPPORTED and the guards isLocatorDenied, isLocatorInvalid, isLocatorUnsupported and isTerminalError. LOCATOR_INVALID and isLocatorInvalid are exported since @fkn/lib 0.9.42. isTerminalError matches the three names that stop the retry loop. Each guard reads nothing but the name, so an error built with that name is enough to show it:
Every call with a deadline ends at it with a TimeoutError, exported from the root, named 'TimeoutError' and minted in your realm, so instanceof works: a locator action, goto, evaluate, addScriptTag and clearCookies. Its message is what the call reported at that moment, unchanged. A locator action retries every failed attempt until its deadline, which is 30,000 ms unless you pass timeout, so for a missing element the message is No elements found, and the last attempt’s error is the cause:
A call that ran out of time: its deadline passed before it settled. One class for every call that
has a deadline, as Playwright's TimeoutError, named 'TimeoutError' so a check by name works as
well as instanceof. It is minted where the call was made, never in the realm that ran it, so it
is an instance of this class in the caller's realm.
message is what the call reported at its deadline, unchanged from the error it replaces. cause
is the last retryable error an attempt met before the deadline, so a locator call that never found
its element says why; it is absent when no attempt failed (one never settled, or the call is not
retried). Not terminal: nothing retries a call past its own deadline.
Locator timeout (30000ms): textContent is the message only when no attempt failed before the deadline, and then there is no cause. A handler that waits for the word timeout in the message will not see it after a missing element, so test the class or the name.
Chrome stops the FKN extension’s service worker whenever it has been idle for a while. The calls that run there are extension.fetch and the root fetch on the extension path, cookies.get, the header rules, and attachFrame and goto() on cookies: 'native'. From the next FKN extension store release, the first after 0.1.54, one of them that a stop cuts off rejects with an Error named BackgroundStoppedError, either before it resolved or, for a fetch, while its body was still arriving, and the next call is answered by the restarted worker. Through 0.1.54 nothing rejects: such a call made after a stop stays pending until the page reloads.
@fkn/lib 0.9.43 exports the name as BACKGROUND_STOPPED and the guard isBackgroundStopped, which reads nothing but the name, so a handler written now already works on that release:
Fetches through the extension. A credentialed fetch (credentials: 'include') and one to the local
network are asked for on the target's host first. A url on the extension's own pages or an FKN
platform host is refused with an Error before anything is asked. A redirect onto an FKN platform
host is not followed: the hop is blocked before it is sent, on both engines and in every spelling,
and should one ever slip the block the final answer is still withheld with an Error. A fetch that
follows redirects is refused before it is sent when the extension could not set the block up.
load() // a GET is harmless to run twice, and the restarted worker answers it
})
The call may or may not have run, so the extension never sends it again, and a retry is yours to decide. Repeat a read like the GET above, and find out what happened before repeating a call that changes something, such as a POST. Both messages are on every error.
The re-mint wraps cloud.fs reads and writes only. The deletes, unlink, rm and rmdir, are not wrapped, in the promise form and the callback form alike. Deleting a missing path therefore rejects with the api’s own Not found, and isNotFound answers false:
A recursive rm goes further: it swallows the failure of every object under the path and reports nothing at all. For a delete, match the message or treat any failure as best effort. Deletes are described under storage.
One row per group of every error, with the test for that group and whether the same call can succeed later:
Group
What to test
Retry
Fetch
a refusal from the library or the extension rejects, so wrap the call. A refusal from the proxy, the server cloud.fetch sends a request through, arrives as a resolved Response, so check response.ok and the body’s error
instanceof FrameWindowBlockedError and FrameWindowRefusedError for a window, instanceof TimeoutError for a deadline, the message prefix, then isTerminalError()
a timed-out handshake or goto, and a blocked window from a fresh click, never a refused target or option
Locators
isLocatorDenied(), isTerminalError(), instanceof TimeoutError, then error.name
a TimeoutError once the page had time to settle, since the loop already retried each attempt inside it, and never a terminal name, which was never retried and will not change
Permissions
error.name === 'PermissionDeniedError'
only through the user
Packages
error.code
a timeout or an unavailable, and an invalid only after fixing the argument it names
Broker
instanceof BrokerUnreachableError, then the FKN: the broker was replaced prefix, and isBackgroundStopped() for a call into the extension
the class after a wait, the replaced broker once, and a stopped extension call when running it twice is harmless. A relayWorker refusal needs the realm fixed rather than a retry
Several rows describe an integration working as intended, such as a refusal at the consent sheet or a missing first backup. Handle those as answers rather than failures.
BackgroundStoppedError: Chrome stopped the extension’s idle worker while a call was out. Raised from the next extension store release, the first after 0.1.54.