Reference

Errors

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 as outside-workspace, symlink, hardlink, or too-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

CodeWhen it firesCommon causes
already-existscreate(), createJson(), move({ overwrite: false }).Target file or directory already at the destination.
hardlinkRead or copy with hardlinks: "reject" saw nlink > 1.File is hardlinked — possibly an alias of an out-of-tree inode.
helper-failedInternal POSIX helper (Python-based fd-relative ops, sidecar lock acquire) failed.Inspect cause for the underlying error.
helper-unavailableHelper could not be spawned at all.Python missing in PATH; restricted sandbox. Library falls back to Node-only path where possible.
insecure-permissionsA 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-pathInput was empty, contained NUL, was an unparseable URL, or otherwise unusable.Caller didn't validate input; input was a network path on Windows.
not-emptyremove() on a non-empty directory.Use replaceDirectoryAtomic or remove children first.
not-fileRead or copy targeted a non-regular file.Target was a directory, FIFO, socket, device.
not-foundThe target does not exist (or its parent does not, with mkdir: false).Typical missing-file case.
not-ownedA secure file owner check failed.File is owned by another UID.
not-removableremove() couldn't unlink/rmdir for a reason other than non-empty.Permissions, device busy, immutable bit.
outside-workspacePath resolves outside the configured root... traversal; absolute path outside the root; symlink resolved out.
path-aliasA path alias check failed (e.g. canonical-real-path moved out of the root).Symlink resolution lands outside the root.
path-mismatchPost-open identity check failed: the opened fd does not match the resolved path.TOCTOU — something else swapped the path between resolve and open.
permission-unverifiedA secure file check could not verify required permissions.Windows ACL inspection failed; POSIX ownership/mode was unavailable.
symlinkPath component is a symlink, policy is reject.Caller followed a symlink they shouldn't have, or symlinks: "reject" is set.
timeoutAn operation with a wall-clock budget overran.Secure file read or timed operation exceeded timeoutMs.
too-largeRead exceeded maxBytes.Caller gave a too-permissive file or didn't size-cap correctly.
unsupported-platformThe 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 by readJson. Carries cause so you can distinguish missing (ENOENT) from invalid (SyntaxError).
  • ArchiveLimitError — thrown by extractArchive when an archive size, entry count, or extracted-byte budget is exceeded. The code field uses ARCHIVE_LIMIT_ERROR_CODE constants (e.g. "ARCHIVE_SIZE_EXCEEDS_LIMIT").
  • ArchiveSecurityError — thrown by extraction when an entry path violates safety rules (traversal, drive prefix, blocked link type). The code field uses ArchiveSecurityErrorCode values.

These are exported from their respective subpaths.

#Why FsSafeError?

Two reasons it isn't a richer hierarchy of subclasses:

  1. Switch on code, don't instanceof a tree. code is a closed string union the TypeScript compiler can exhaust-check. Subclasses make instanceof ladders that drift over time.
  2. One catch handler. Library callers often want a single "is this an fs-safe failure?" gate before deciding what to do — instanceof FsSafeError plus a switch is the cleanest expression of that.

#See also