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 (Python-based fd-relative ops, sidecar lock acquire) failed. | Inspect cause for the underlying error. |
helper-unavailable | Helper could not be spawned at all. | Python missing in PATH; restricted sandbox. Library falls back to Node-only path where possible. |
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.