Skip to content

Handling errors

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, 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:

app.ts
try {
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')
} catch (
var error: unknown
error
) {
const {
const name: string
name
,
const message: string
message
,
const code: string | undefined
code
} =
var error: unknown
error
as
interface Error
Error
& {
code?: string | undefined
code
?: string }
const name: string
name
// 'Error', so there is no class to test, only the sentence
const message: string
message
// 'storage: api unreachable', the prefix you match
const code: 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:

app.ts
import {
(alias) namespace cloud
import cloud
cloud
} 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'
try {
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
('backup/library.bin')
} catch (
var error: unknown
error
) {
function isNotFound(error: unknown): boolean

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

isNotFound
(
var error: unknown
error
) // true for both 'Not found' and 'storage: read failed (404)'
var error: unknown
error
instanceof
class StorageLockedError
StorageLockedError
// false, locked is its own class and never reads as absence
}

isNotFound answers true for both wordings. StorageLockedError carries storage locked: this account stores encrypted data, unlock it from the FKN card or unlock(), a sentence that matches neither on purpose. The difference matters: a path with nothing at it is safe to write a first backup to, and a path you could not read is not.

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:

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, minted in your 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 // start empty
if (
error: unknown
error
instanceof
class StorageLockedError
StorageLockedError
) return
const promptUnlock: () => Promise<null>
promptUnlock
() // ask, then read again
// the rest of the broker's errors, by 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
() // retry later
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
() // routed to the new broker already
// locators and frames, by name, through the guards
if (
function isLocatorDenied(error: unknown): boolean
isLocatorDenied
(
error: unknown
error
)) return
const explainRefusal: () => null
explainRefusal
() // a gate said no
if (
function isTerminalError(error: unknown): boolean
isTerminalError
(
error: unknown
error
)) return
const giveUp: () => null
giveUp
() // retrying changes nothing
// the consent refusal, its own name, matched by neither guard
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 class minted here
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

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 there, the extension rejects with Permission denied: <key> (<scope>) under the name PermissionDeniedError. Neither locator guard looks for that name:

app.ts
try {
await
const frame: Frame
frame
.
locator: (selector: string) => Locator$1<Extended<{
readonly element: {
readonly selectors: {
readonly locator: {
readonly to: "element";
readonly css: true;
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "descend";
};
};
readonly frameLocator: {
readonly to: "frame";
readonly css: true;
readonly barrier: "down";
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "down";
};
};
readonly getByRole: {
readonly to: "element";
readonly resolve: (context: LocatorContext, role: string) => Element[];
readonly render: (role: unknown) => {
fragment: string;
};
};
readonly getByText: {
readonly to: "element";
readonly resolve: (context: LocatorContext, text: string) => Element[];
readonly render: (text: unknown) => {
fragment: string;
};
};
readonly getByTestId: {
readonly to: "element";
readonly resolve: (context: LocatorContext, testId: string) => Element[];
readonly render: (testId: unknown) => {
fragment: string;
};
};
readonly first: {
readonly to: "element";
readonly resolve: (context: LocatorContext) => Element[];
readonly render: () => {
fragment: string;
};
};
readonly nth: {
readonly to: "element";
readonly resolve: (context: LocatorContext, index: number) => Element[];
readonly render: (index: unknown) => {
fragment: string;
};
};
};
readonly operations: {
readonly click: {
readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void;
};
readonly fill: {
readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void;
};
readonly hover: {
readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void;
};
readonly textContent: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string;
};
readonly getAttribute: {
readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null;
};
readonly isVisible: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean;
};
readonly count: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number;
};
readonly exists: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean;
};
};
};
readonly frame: {
readonly selectors: {
readonly locator: {
readonly to: "element";
readonly css: true;
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "descend";
};
};
readonly frameLocator: {
readonly to: "frame";
readonly css: true;
readonly barrier: "down";
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "down";
};
};
readonly owner: {
readonly to: "frame";
readonly barrier: "up";
readonly resolve: (_context: LocatorContext) => Element[];
readonly render: () => {
fragment: string;
separator: "up";
};
};
};
readonly operations: {
readonly addStyleTag: {
readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void;
};
readonly fetch: {
readonly kind: ChainKind;
readonly ...
locator
('#play').
click: (options?: PositionOptions | undefined) => Promise<void>
click
({
reason?: string | undefined
reason
: 'Start the trailer' })
} catch (
var error: unknown
error
) {
const {
const name: string
name
,
const message: string
message
} =
var error: unknown
error
as
interface Error
Error
function isLocatorDenied(error: unknown): boolean
isLocatorDenied
(
var error: unknown
error
) // false, even though the user refused
function isTerminalError(error: unknown): boolean
isTerminalError
(
var error: unknown
error
) // false
const name: string
name
// 'PermissionDeniedError'
const message: string
message
// 'Permission denied: 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 keys and scopes are described under permissions and consent.

@fkn/lib re-exports the constants LOCATOR_DENIED, LOCATOR_ERROR and LOCATOR_UNSUPPORTED and the guards isLocatorDenied, isLocatorUnsupported and isTerminalError. It does not export LOCATOR_INVALID, isLocatorInvalid or locatorInvalidError. isTerminalError still matches the invalid name. The guard reads nothing but the name, so an error built with that name is enough to show it:

app.ts
import { isLocatorInvalid } from '@fkn/lib'
Error ts(2305) ― Module '"@fkn/lib"' has no exported member 'isLocatorInvalid'.
import {
const isTerminalError: (error: unknown) => boolean
isTerminalError
} from '@fkn/lib'
const
const invalid: Error & {
name: string;
}
invalid
=
var Object: ObjectConstructor

Provides functionality common to all JavaScript objects.

Object
.
ObjectConstructor.assign<Error, {
name: string;
}>(target: Error, source: {
name: string;
}): Error & {
name: string;
} (+3 overloads)

Copy the values of all of the enumerable own properties from one or more source objects to a target object. Returns the target object.

@paramtarget The target object to copy to.

@paramsource The source object from which to copy properties.

assign
(new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
('fetch: url must be a string'), {
name: string
name
: 'LocatorInvalidError' })
function isTerminalError(error: unknown): boolean
isTerminalError
(
const invalid: Error & {
name: string;
}
invalid
) // true, the invalid name stops the loop like the other two

Use the guard rather than comparing against a name whose constant you cannot import. The locator errors are described under locators and actions.

A locator action retries every failed attempt until its deadline, which is 30,000 ms unless you pass timeout. It then rethrows the last attempt’s error. For a missing element that error is No elements found:

app.ts
try {
await
const frame: Frame
frame
.
locator: (selector: string) => Locator$1<Extended<{
readonly element: {
readonly selectors: {
readonly locator: {
readonly to: "element";
readonly css: true;
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "descend";
};
};
readonly frameLocator: {
readonly to: "frame";
readonly css: true;
readonly barrier: "down";
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "down";
};
};
readonly getByRole: {
readonly to: "element";
readonly resolve: (context: LocatorContext, role: string) => Element[];
readonly render: (role: unknown) => {
fragment: string;
};
};
readonly getByText: {
readonly to: "element";
readonly resolve: (context: LocatorContext, text: string) => Element[];
readonly render: (text: unknown) => {
fragment: string;
};
};
readonly getByTestId: {
readonly to: "element";
readonly resolve: (context: LocatorContext, testId: string) => Element[];
readonly render: (testId: unknown) => {
fragment: string;
};
};
readonly first: {
readonly to: "element";
readonly resolve: (context: LocatorContext) => Element[];
readonly render: () => {
fragment: string;
};
};
readonly nth: {
readonly to: "element";
readonly resolve: (context: LocatorContext, index: number) => Element[];
readonly render: (index: unknown) => {
fragment: string;
};
};
};
readonly operations: {
readonly click: {
readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void;
};
readonly fill: {
readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void;
};
readonly hover: {
readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void;
};
readonly textContent: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string;
};
readonly getAttribute: {
readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null;
};
readonly isVisible: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean;
};
readonly count: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number;
};
readonly exists: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean;
};
};
};
readonly frame: {
readonly selectors: {
readonly locator: {
readonly to: "element";
readonly css: true;
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "descend";
};
};
readonly frameLocator: {
readonly to: "frame";
readonly css: true;
readonly barrier: "down";
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "down";
};
};
readonly owner: {
readonly to: "frame";
readonly barrier: "up";
readonly resolve: (_context: LocatorContext) => Element[];
readonly render: () => {
fragment: string;
separator: "up";
};
};
};
readonly operations: {
readonly addStyleTag: {
readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void;
};
readonly fetch: {
readonly kind: ChainKind;
readonly ...
locator
('h1').
textContent: (_options?: LocatorOptions | undefined) => Promise<string>
textContent
()
} catch (
var error: unknown
error
) {
const {
const name: string
name
,
const message: string
message
} =
var error: unknown
error
as
interface Error
Error
const message: string
message
// 'No elements found', after 30 seconds
const name: string
name
// 'LocatorError', the last attempt's error rather than a timeout
}

Locator timeout (30000ms): textContent appears only when no attempt failed before the deadline. A handler that waits for the word timeout will not see it after a missing element.

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:

app.ts
import {
(alias) namespace cloud
import cloud
cloud
} 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
} from '@fkn/lib/cloud/fs'
try {
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
.
unlink: (path: import("node:fs").PathLike) => Promise<void>
unlink
('backup/library.bin')
} catch (
var error: unknown
error
) {
const {
const message: string
message
} =
var error: unknown
error
as
interface Error
Error
const message: string
message
// 'Not found', the api's own sentence
function isNotFound(error: unknown): boolean

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

isNotFound
(
var error: unknown
error
) // false, unlink does not run the re-mint
}

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:

GroupWhat to testRetry
Fetcha 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 errora 429 or a 502 in the body, or no proxy being available, never a refused target
Socketsan argument or state refusal throws at the call, and a connect, bind or listen failure arrives as the message prefix on the error eventa timed-out connect or a capacity ceiling, never a refused address
StorageisNotFound(), instanceof StorageLockedError, then the storage: prefixunreachable and Concurrent update, retry, locked after unlock(), never absence
Encryptionthe three E2E_* prefixes, then the api’s own sentencesnever on fkn:e2e-stale-epoch or fkn:e2e-integrity, a write refused with Superseded key at once, and one refused with This account has no encryption keys yet after re-enrolling
Framesthe message prefix, then isTerminalError()a timed-out handshake or goto, never a refusal
LocatorsisLocatorDenied(), isTerminalError(), then error.namenot for a plain LocatorError, which the loop already retried, and not for a terminal name, which was never retried and will not change
Permissionserror.name === 'PermissionDeniedError'only through the user
Packageserror.codea timeout or an unavailable, and an invalid only after fixing the argument it names
Brokerinstanceof BrokerUnreachableError, then the FKN: the broker was replaced prefixthe class after a wait, and the replaced broker once. 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.

The six broker and lifecycle rows, each with its cause and its fix:

MessageWhat happenedWhat to do
@fkn/lib: no broker connection within <ms>ms, so <what> could not be requestedNo broker connection settled inside the deadline of 8,000 ms, or 1,000 ms after a miss. The usual cause is a worker nobody relayed.Call await relayWorker(worker, { unregisterSignal }) from the page, as shown under workers.
FKN: the broker was replaced while this call was pending; retry itAn update of the shell, the FKN surface that can update and reload the page, reloaded the broker frame while a call was in flight.Retry once. The replacement is described under errors and lifecycle.
FKN @fkn/lib: relayWorker must be called from the main threadrelayWorker ran inside a worker.Call it from the page.
FKN @fkn/lib: relayWorker found no FKN transport in this realmThis realm holds neither a mounted broker frame nor a port from a parent FKN realm.Import the library on a page that mounts the broker frame first, as described under how it works.
storage: api unreachableThe storage api never answered at all, which is different from the api answering with an error.Keep the local copy and retry later.
webvpn: socket capacity reachedThe relay, the server that holds the real socket for net and dgram, reached a capacity ceiling.Close a socket this app holds to free a slot. The ceilings are listed under limits and timeouts.

Every other message has a row on every error. The calls that wait with no deadline are listed on errors and lifecycle.