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

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: connect the app again to open encrypted data, 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.

@param ― onrejected The callback to execute when the Promise is rejected.

@returns ― A 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 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:

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
function isPermissionDenied(error: unknown): boolean
isPermissionDenied
(
var error: unknown
error
) // true, the one guard that reads this name
const name: string
name
// 'PermissionDeniedError'
const message: 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:

app.ts
import {
const isLocatorInvalid: (error: unknown) => boolean
isLocatorInvalid
,
const isTerminalError: (error: unknown) => boolean
isTerminalError
,
const LOCATOR_INVALID: "LocatorInvalidError"
LOCATOR_INVALID
} 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.

@param ― target The target object to copy to.

@param ― source 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
:
const LOCATOR_INVALID: "LocatorInvalidError"
LOCATOR_INVALID
})
function isLocatorInvalid(error: unknown): boolean
isLocatorInvalid
(
const invalid: Error & {
name: string;
}
invalid
) // true
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 guards rather than comparing names by hand. The locator errors are described under locators and actions.

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:

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
) {
if (!(
var error: unknown
error
instanceof
class TimeoutError

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.

TimeoutError
)) throw
var error: unknown
error
var error: TimeoutError
error
.
Error.message: string
message
// 'No elements found', after 30 seconds
const
const cause: Error
cause
=
var error: TimeoutError
error
.
Error.cause?: unknown
cause
as
interface Error
Error
const cause: Error
cause
.
Error.name: string
name
// 'LocatorError', the last attempt's error
}

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:

app.ts
import {
(alias) namespace extension
import extension
extension
,
const isBackgroundStopped: (error: unknown) => boolean

Whether error is a BackgroundStoppedError, matched on name since it may arrive as a revived Error or a plain record.

isBackgroundStopped
} from '@fkn/lib'
const
const load: () => Promise<string>
load
= async ():
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<string> => {
const
const response: Response
response
= await
(alias) namespace extension
import extension
extension
.
extension_d_exports.fetch(input: RequestInfo | URL, init?: extension.FetchInit): Promise<Response>
export extension_d_exports.fetch

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.

fetch
('https://example.org/api/catalog.json')
return
const response: Response
response
.
Body.text(): Promise<string>
text
() // a body the stop cut off rejects here, under the same name
}
const
const catalog: string
catalog
= await
const load: () => Promise<string>
load
().
Promise<string>.catch<string>(onrejected?: ((reason: any) => string | PromiseLike<string>) | null | undefined): Promise<string>

Attaches a callback for only the rejection of the Promise.

@param ― onrejected The callback to execute when the Promise is rejected.

@returns ― A Promise for the completion of the callback.

catch
((
error: any
error
) => {
if (!
function isBackgroundStopped(error: unknown): boolean

Whether error is a BackgroundStoppedError, matched on name since it may arrive as a revived Error or a plain record.

isBackgroundStopped
(
error: any
error
)) throw
error: any
error
return
const load: () => Promise<string>
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:

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
Framesinstanceof 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
LocatorsisLocatorDenied(), isTerminalError(), instanceof TimeoutError, then error.namea 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
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 prefix, and isBackgroundStopped() for a call into the extensionthe 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.

The seven 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.
the FKN extension's background stopped before it answeredBackgroundStoppedError: 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.Retry with your own call when running it twice is harmless, see a call the extension never finished.
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.