7.0 KiB
Errors
Every failure that's the library's job to surface lands as an FsSafeError with a closed code union you can branch on. Catch by code, not by message text — messages may change, codes will not.
import { FsSafeError, type FsSafeErrorCode } from "@openclaw/fs-safe";
Shape
class FsSafeError extends Error {
readonly name: "FsSafeError";
readonly code: FsSafeErrorCode;
readonly category: "policy" | "operational";
constructor(code: FsSafeErrorCode, message: string, options?: { cause?: unknown });
}
cause is available through the standard Error cause property when the failure was triggered by a NodeJS.ErrnoException (e.g. a wrapped EACCES). Inspect it for the original code / errno / syscall if you need finer-grained reporting.
category separates caller-policy failures from operational failures:
"policy"— unsafe input or target state, such asoutside-workspace,symlink,hardlink, ortoo-large."operational"— environment/runtime failures, such as helper startup, platform support, timeout, or unverifiable permissions.
Code union
type FsSafeErrorCode =
| "already-exists"
| "hardlink"
| "helper-failed"
| "helper-unavailable"
| "insecure-permissions"
| "invalid-path"
| "not-empty"
| "not-file"
| "not-found"
| "not-owned"
| "not-removable"
| "outside-workspace"
| "path-alias"
| "path-mismatch"
| "permission-unverified"
| "symlink"
| "timeout"
| "too-large"
| "unsupported-platform";
Code reference
| Code | When it fires | Common causes |
|---|---|---|
already-exists |
create(), createJson(), move({ overwrite: false }). |
Target file or directory already at the destination. |
hardlink |
Read or copy with hardlinks: "reject" saw nlink > 1. |
File is hardlinked — possibly an alias of an out-of-tree inode. |
helper-failed |
Internal POSIX helper failed after startup. | Inspect cause; retrying may be unsafe if the operation may have partially completed. |
helper-unavailable |
Persistent Python helper was disabled or could not be spawned. | FS_SAFE_PYTHON_MODE=off, Python missing in PATH, restricted sandbox. auto falls back where possible; require fails closed. |
insecure-permissions |
A secure file or path permission check found a mode/ACL that allows broader access than requested. | File or directory is group/world writable/readable; Windows ACL grants broad read. |
invalid-path |
Input was empty, contained NUL, was an unparseable URL, or otherwise unusable. | Caller didn't validate input; input was a network path on Windows. |
not-empty |
remove() on a non-empty directory. |
Use replaceDirectoryAtomic or remove children first. |
not-file |
Read or copy targeted a non-regular file. | Target was a directory, FIFO, socket, device. |
not-found |
The target does not exist (or its parent does not, with mkdir: false). |
Typical missing-file case. |
not-owned |
A secure file owner check failed. | File is owned by another UID. |
not-removable |
remove() couldn't unlink/rmdir for a reason other than non-empty. |
Permissions, device busy, immutable bit. |
outside-workspace |
Path resolves outside the configured root. | .. traversal; absolute path outside the root; symlink resolved out. |
path-alias |
A path alias check failed (e.g. canonical-real-path moved out of the root). | Symlink resolution lands outside the root. |
path-mismatch |
Post-open identity check failed: the opened fd does not match the resolved path. | TOCTOU — something else swapped the path between resolve and open. |
permission-unverified |
A secure file check could not verify required permissions. | Windows ACL inspection failed; POSIX ownership/mode was unavailable. |
symlink |
Path component is a symlink, policy is reject. |
Caller followed a symlink they shouldn't have, or symlinks: "reject" is set. |
timeout |
An operation with a wall-clock budget overran. | Secure file read or timed operation exceeded timeoutMs. |
too-large |
Read exceeded maxBytes. |
Caller gave a too-permissive file or didn't size-cap correctly. |
unsupported-platform |
The requested operation is not supported on the current platform. | E.g. POSIX-only helper invoked on Windows. |
Branching
import { FsSafeError } from "@openclaw/fs-safe";
try {
await fs.write("../escape.txt", "x");
} catch (err) {
if (!(err instanceof FsSafeError)) throw err;
switch (err.code) {
case "outside-workspace":
return reply(400, "path escapes workspace");
case "already-exists":
return reply(409, "exists");
case "too-large":
return reply(413, "too large");
case "not-found":
return reply(404, "missing");
case "symlink":
case "hardlink":
case "path-mismatch":
case "path-alias":
return reply(400, "unsafe path");
default:
throw err;
}
}
The compiler will flag missing cases when you exhaust the union — keep your switch up-to-date as the library adds new codes.
Distinguishing from NodeJS.ErrnoException
Some failures bubble up as native Node errors (e.g. EACCES, EISDIR, EBUSY) when they don't map cleanly to a library code. Inspect both:
import { FsSafeError } from "@openclaw/fs-safe";
try {
await op();
} catch (err) {
if (err instanceof FsSafeError) {
handleFsSafe(err);
return;
}
if ((err as NodeJS.ErrnoException).code === "EACCES") {
handleAccess();
return;
}
throw err;
}
A common pattern is to wrap your domain code in a single try/catch that maps both shapes to your application's typed error format.
Specialty errors
A handful of helpers throw their own typed errors instead of FsSafeError:
JsonFileReadError— thrown byreadJson. Carriescauseso you can distinguish missing (ENOENT) from invalid (SyntaxError).ArchiveLimitError— thrown byextractArchivewhen an archive size, entry count, or extracted-byte budget is exceeded. Thecodefield usesARCHIVE_LIMIT_ERROR_CODEconstants (e.g."ARCHIVE_SIZE_EXCEEDS_LIMIT").ArchiveSecurityError— thrown by extraction when an entry path violates safety rules (traversal, drive prefix, blocked link type). Thecodefield usesArchiveSecurityErrorCodevalues.
These are exported from their respective subpaths.
Why FsSafeError?
Two reasons it isn't a richer hierarchy of subclasses:
- Switch on
code, don'tinstanceofa tree.codeis a closed string union the TypeScript compiler can exhaust-check. Subclasses makeinstanceofladders that drift over time. - One catch handler. Library callers often want a single "is this an
fs-safefailure?" gate before deciding what to do —instanceof FsSafeErrorplus a switch is the cleanest expression of that.
See also
root()— every method documents the codes it can throw.- Reading — read-path codes.
- Writing — write-path codes.
- Archive extraction —
ArchiveLimitErrorandArchiveSecurityError.