# Error codes

> fs.ErrorCode to POSIX errno mapping, structured zap hints on failure, constraint violations, and reading stderr when tools report EINVAL/EACCES.

- Repository: timescale/tigerfs
- GitHub: https://github.com/timescale/tigerfs
- Human docs: https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3
- Complete Markdown: https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/llms-full.txt

## Source Files

- `internal/tigerfs/fs/errors.go`
- `internal/tigerfs/fs/errors_test.go`
- `internal/tigerfs/fuse/control_files.go`
- `internal/tigerfs/logging/logging.go`
- `docs/spec.md`

---

---
title: "Error codes"
description: "fs.ErrorCode to POSIX errno mapping, structured zap hints on failure, constraint violations, and reading stderr when tools report EINVAL/EACCES."
---

TigerFS surfaces filesystem failures as POSIX `errno` values to shells and tools (`mv`, `cp`, `echo > file`), while the shared `fs` package classifies errors with backend-agnostic `ErrorCode` values wrapped in `*FSError`. FUSE and NFS adapters translate `FSError` to `syscall.Errno` or `os` errors; structured detail—including optional `Hint` fields—is written to **stderr** via zap before the syscall returns.

## Error flow

FUSE and NFS cannot return arbitrary error strings to callers. The design logs human-oriented detail first, then returns a standard errno.

```mermaid
sequenceDiagram
    participant Tool as Shell / tool
    participant Adapter as fuse.FSAdapter or nfs.OpsFilesystem
    participant Ops as fs.Operations
    participant DB as PostgreSQL

    Tool->>Adapter: VFS op (write, rename, stat)
    Adapter->>Ops: ReadDir / Stat / WriteFile / ...
    Ops->>DB: SQL (when needed)
    DB-->>Ops: success or driver error
    Ops-->>Adapter: nil or *FSError
    Note over Adapter: logging.Error / Debug<br/>with hint, message, cause
    Adapter-->>Tool: errno (e.g. EINVAL, EACCES)
```

On Linux (FUSE), `FSAdapter.ErrorToErrno` is the canonical mapping used by migrated `OpsNode` paths and adapter helpers. macOS (NFS) maps a subset of codes to `os.ErrNotExist`, `os.ErrPermission`, and `os.ErrInvalid`; other `FSError` codes may collapse differently at the NFS layer.

## `ErrorCode` and `FSError`

`ErrorCode` is an `int` enum defined in `internal/tigerfs/fs/errors.go`. Each constant documents its intended POSIX mapping in comments; adapters implement that mapping in `fuse/adapter.go`.

| `ErrorCode` | Constant | POSIX errno | Typical causes |
|-------------|----------|-------------|----------------|
| 0 | `ErrNone` | success (`0`) | No error |
| 1 | `ErrNotExist` | `ENOENT` | Missing path, row, column, or DDL session |
| 2 | `ErrPermission` | `EACCES` | Read-only view, reserved TigerFS name, immutable `.history/`, undo across migration boundary, DB privilege denial |
| 3 | `ErrInvalidPath` | `EINVAL` | Malformed pipeline path, wrong capability order, invalid DDL path |
| 4 | `ErrInvalidFormat` | `EINVAL` | Unparseable row file (JSON/TSV/CSV/YAML) |
| 5 | `ErrInvalidOperation` | `EPERM` | Operation not allowed (e.g. `Readlink` on non-symlink) |
| 6 | `ErrReadOnly` | `EROFS` | Write on read-only mount |
| 7 | `ErrNotEmpty` | `ENOTEMPTY` | `rmdir` on non-empty directory |
| 8 | `ErrAlreadyExists` | `EEXIST` | Path or row already exists (`ErrExists` alias) |
| 9 | `ErrIO` | `EIO` | DB connection/query failure, unexpected SQL error, many wrapped `Cause` errors |
| 10 | `ErrInternal` | `EIO` | Unexpected internal failure |
| 11 | `ErrNotImplemented` | `ENOSYS` | Feature not implemented |
| 12 | `ErrInvalidArgument` | `EINVAL` | Invalid argument value (grouped with path/format errors in FUSE) |

`FSError` carries programmatic and user-facing fields:

| Field | Role |
|-------|------|
| `Code` | `ErrorCode` for adapter mapping |
| `Message` | Short description; also `error.Error()` when no `Cause` |
| `Cause` | Underlying error (`Unwrap` for `errors.Is` / `As`) |
| `Hint` | Actionable guidance logged to stderr before errno return |

Constructors such as `NewNotExistError`, `NewPermissionError`, and `NewInvalidPathError` populate `Message`; `NewInvalidPathError` also copies `reason` into `Hint`.

<Note>
`ErrNotExist` is treated as a normal outcome for many lookups (tab completion, `stat` before `mkdir`). `ErrorToErrno` logs it at **debug** only, not `Error`, to avoid noise.
</Note>

## FUSE mapping (`ErrorToErrno`)

`fuse.FSAdapter.ErrorToErrno` converts `*FSError` to `syscall.Errno` and applies logging rules:

| Condition | Log level | Fields |
|-----------|-----------|--------|
| `Code == ErrNotExist` | `logging.Debug` | `message`, optional `hint`, `cause` |
| `Hint != ""` (other codes) | `logging.Error` | `message`, `hint`, `cause` |
| `Cause != nil`, no hint | `logging.Debug` | `message`, `cause` |
| Otherwise | (no log) | — |

Mapping switch (default unknown codes → `EIO`):

```
ErrNotExist          → ENOENT
ErrPermission        → EACCES
ErrInvalidPath,
  ErrInvalidFormat,
  ErrInvalidArgument → EINVAL
ErrInvalidOperation  → EPERM
ErrReadOnly          → EROFS
ErrNotEmpty          → ENOTEMPTY
ErrAlreadyExists     → EEXIST
ErrIO, ErrInternal   → EIO
ErrNotImplemented    → ENOSYS
(default)            → EIO
```

Unit tests in `fuse/adapter_test.go` lock these mappings for the covered codes.

## NFS mapping differences

`nfs.OpsFilesystem` does not call `ErrorToErrno`. For `Stat`, `ReadDir`, and `Rename` it maps:

| `ErrorCode` | Go error returned |
|-------------|-------------------|
| `ErrNotExist` | `os.ErrNotExist` |
| `ErrPermission` | `os.ErrPermission` |
| `ErrInvalidPath`, `ErrInvalidArgument` | `os.ErrInvalid` |
| Other (including `ErrIO`, `ErrInvalidFormat`) | Often `os.ErrNotExist` on directory ops, or wrapped `fmt.Errorf` on writes |

Create/Open paths log `message`, `hint`, and `cause` at `Error` and return `os.ErrInvalid` when `ValidateCreate` rejects a path (e.g. bare row path without format suffix on macOS).

<Warning>
Do not assume NFS and FUSE return identical errno for every `FSError`. Prefer stderr logs and `Hint` when diagnosing macOS mounts.
</Warning>

## PostgreSQL and constraint violations

The specification (`docs/spec.md`) documents the user-visible contract: standard errno at the syscall boundary, full PostgreSQL detail in logs.

| PostgreSQL / domain situation | Spec errno | `ErrorCode` in shared `fs` layer (when used) |
|------------------------------|------------|-----------------------------------------------|
| Row not found | `ENOENT` | `ErrNotExist` |
| Table/view privilege denied | `EACCES` | `ErrPermission` |
| NOT NULL / FK / check violation | `EACCES` (generic message to tool) | Often `ErrIO` with `Cause` = driver error on `WriteFile`/`InsertRow`; legacy FUSE nodes may map substring `"constraint"` → `EACCES` |
| Connection / timeout / other SQL | `EIO` | `ErrIO` + `Cause` |

**Pre-insert validation:** `db.ValidateConstraints` checks NOT NULL (no default) and UNIQUE before some incremental row commits (`fuse/partial.go`). Violations return Go errors with messages like `NOT NULL constraint violation on column email`; these are logged at `Error` in partial-row commit paths.

**Legacy FUSE paths** (still present alongside `fs.Operations`):

- `TableNode` delete/rmdir: errors containing `foreign key` or `constraint` → `EACCES`
- `RowDirectoryNode` unlink on NOT NULL column without default → `EACCES`
- `RowFileNode` flush failures → `EIO` (constraint text only in logs)

**Shared `fs.Operations` path:** many database failures become `ErrIO` with the driver error in `Cause` (e.g. `failed to insert row`). Tools see `EIO`; read stderr for `null value in column ... violates not-null constraint` and similar.

<Info>
Example spec workflow: `echo 'Name' > /mnt/db/users/125/name` when `email` is NOT NULL may return exit code **13** (`EACCES`) on some code paths, while the mount process logs the real constraint message on stderr.
</Info>

## `Hint` field patterns

`Hint` is set at the point of failure so adapters can log it before returning errno. Common sources:

| Area | Example hint |
|------|----------------|
| Path parsing (`fs/path.go`) | `filters must come before .order/ in the path` |
| Row create validation | `retry as "1.json", "1.tsv", or "1.csv" (bare-path writes conflict ...)` |
| DDL staging | `run validation with: touch /.create/<name>/.test` |
| Synth reserved names | `choose a different filename to avoid collision with TigerFS control directories` |
| History / undo | `history files are archived versions and cannot be modified`; migration boundary text from metadata `Description` |
| File-first routing | `use mount/.tables/<table>/... for data-first access to the backing table` |

Reserved TigerFS capability names (`.filter`, `.history`, `.undo`, etc.) return `ErrPermission` → `EACCES`, matching the spec reserved-name rule.

## Logging and reading stderr

The `tigerfs` CLI initializes zap in `logging.Init` from `--log-level` (`debug`, `info`, `warn`, `error`; default `warn`). Logs go to **stderr** unless `--log-file` is set. `--log-format json` emits structured lines; production-style configs use a compact JSON shape with a `message` key.

<Steps>
<Step title="Reproduce with verbose logging">
Run the mount in the foreground with debug logging:

```bash
tigerfs mount --foreground --log-level debug postgres://host/db /mnt/db
```

</Step>
<Step title="Run the failing command in another terminal">
```bash
mv 1-foo-o.md a-foo-o.md
# or: echo 'x' > /mnt/db/users/125/name
echo $?   # 13 = EACCES, 22 = EINVAL
```

</Step>
<Step title="Read the mount process stderr">
Look for JSON or text lines immediately before the tool’s errno message:

```json
{"message":"invalid path ...","hint":"limit must be a positive integer"}
```

When `Hint` is set, `ErrorToErrno` logs at `Error` with `hint` as a zap field. The shell only prints the generic errno string (e.g. `Invalid argument`, `Permission denied`).

</Step>
</Steps>

| Tool exit / errno | Value | Often means |
|-------------------|-------|-------------|
| `EINVAL` | 22 | Bad path grammar, bad format, invalid create path, NFS `ValidateCreate` rejection |
| `EACCES` | 13 | Permission, read-only view, reserved name, constraint (legacy paths), undo boundary |
| `EPERM` | 1 | `ErrInvalidOperation` (wrong op type) |
| `ENOENT` | 2 | Missing file/row/column |
| `EIO` | 5 | DB/network failure; check `Cause` in logs |
| `ENOSYS` | 38 | Not implemented |

<Tip>
FUSE returns errno only—never SQL text. Always correlate the user command with the **tigerfs** process stderr from the same time window, not only the shell’s one-line `mv: ...` message.
</Tip>

## errno quick reference for agents

When automating against a mount:

1. Treat non-zero exit codes as `errno`, not as PostgreSQL error codes.
2. On `EINVAL` or `EACCES`, search mount stderr for `"hint"` or `"message"` (JSON) or the plain message (text format).
3. Prefer fixing path grammar and capability order (see capability directories docs) before retrying writes.
4. On `EIO`, inspect `Cause` in logs for connection, timeout, or constraint strings from the driver.
5. Use `ErrNotExist` outcomes for existence checks without treating them as mount failures.

## Related pages

<CardGroup>
<Card title="Filesystem as API" href="/filesystem-as-api">
Path types, dot-directories, and how operations become SQL.
</Card>
<Card title="Capability directories" href="/capability-directories">
Pipeline grammar errors that surface as EINVAL with path hints.
</Card>
<Card title="DDL staging" href="/ddl-staging">
`.test` / `.commit` validation errno and staging control files.
</Card>
<Card title="History, savepoints, and undo" href="/history-savepoints-undo">
Undo boundary refusals (ErrPermission / EACCES) and migration metadata.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Stale mounts, `--log-level debug`, and common errno recovery.
</Card>
<Card title="Develop and test" href="/develop-and-test">
`adapter_test.go`, integration tests, and error regression coverage.
</Card>
</CardGroup>
