# Error codes

> Complete `RiftError` code catalog from FFI and TypeScript bindings, CLI user-facing messages, and path-bearing error variants.

- Repository: anomalyco/rift
- GitHub: https://github.com/anomalyco/rift
- Human docs: https://grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662
- Complete Markdown: https://grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/llms-full.txt

## Source Files

- `npm/rift-snapshot/index.d.ts`
- `crates/ffi/src/lib.rs`
- `crates/core/src/lib.rs`
- `crates/cli/src/main.rs`

---

---
title: "Error codes"
description: "Complete `RiftError` code catalog from FFI and TypeScript bindings, CLI user-facing messages, and path-bearing error variants."
---

Rift surfaces failures through a single snake_case `code` string. The Rust core defines `rift::Error`; the FFI layer maps each variant to a code and optional `path`; the `rift-snapshot` package throws `RiftError` with the same fields. The CLI prints customized guidance for three workspace-state codes and a separate `ForceRequired` guard that never appears in FFI or JavaScript.

```mermaid
sequenceDiagram
  participant App as Application
  participant JS as rift-snapshot
  participant FFI as rift_ffi_call
  participant Core as rift::Error

  App->>JS: init / create / remove / ...
  JS->>FFI: JSON request
  FFI->>Core: Manager operation
  alt success
    Core-->>FFI: Ok(value)
    FFI-->>JS: {"status":"ok","value":...}
    JS-->>App: result
  else core failure
    Core-->>FFI: Err(Error)
    FFI-->>JS: {"status":"error","error":{code,message,path?}}
    JS-->>App: throw RiftError
  else protocol failure
    FFI-->>JS: {"status":"error","error":{code,message}}
    JS-->>App: throw RiftError
  end
```

## Error object shape

FFI error responses use this JSON structure:

```json
{
  "status": "error",
  "error": {
    "code": "workspace_not_initialized",
    "message": "workspace is not initialized: /tmp/app",
    "path": "/tmp/app"
  }
}
```

The `path` field is omitted when the variant carries no filesystem location. Protocol-layer failures (`invalid_request`, `panic`, `serialization`) never include `path`.

<TypeScript bindings>

```ts
export class RiftError extends Error {
  code: RiftErrorCode
  path?: string
  constructor(input: { code: RiftErrorCode; message: string; path?: string })
}
```

Both Node and Bun bindings parse the FFI JSON response and throw `new RiftError(response.error)` when `status === "error"`. The `message` string always comes from the Rust `Display` implementation (or FFI protocol text); `hook_failed` and `invalid_config` embed extra detail in `message` only.

</TypeScript bindings>

<Note>
Binding load failures are ordinary JavaScript `Error` objects, not `RiftError`: unsupported platform, missing prebuilt library, or a null FFI response.
</Note>

## Complete code catalog

### Core domain codes

These codes originate in `rift::Error` and are shared across CLI, FFI, and JavaScript.

| Code | Core variant | `path` in FFI/JS | Default `message` template |
|------|--------------|------------------|----------------------------|
| `io` | `Io` | — | Underlying `std::io::Error` text |
| `database` | `Database` | — | Underlying `rusqlite::Error` text |
| `walk` | `Walk` | — | Underlying `walkdir::Error` text |
| `invalid_path` | `Path` | — | `invalid path: {detail}` |
| `cow_unavailable` | `CowUnavailable` | — | `copy-on-write cloning unavailable: {detail}` |
| `initialization_required` | `InitializationRequired` | source path | `workspace requires initialization: {path}` |
| `workspace_not_initialized` | `WorkspaceNotInitialized` | queried path | `workspace is not initialized: {path}` |
| `missing_marker` | `MissingMarker` | directory path | `rift marker is missing: {path}` |
| `unsupported_entry` | `UnsupportedEntry` | entry path | `unsupported filesystem entry: {path}` |
| `unsafe_git` | `UnsafeGit` | — | `unsafe Git source: {detail}` |
| `not_managed` | `NotManaged` | path | `directory is not managed by rift: {path}` |
| `marker_mismatch` | `MarkerMismatch` | directory path | `rift marker does not match the registry at: {path}` |
| `unknown_marker` | `UnknownMarker` | directory path | `rift marker belongs to an unknown registry entry at: {path}` |
| `already_exists` | `AlreadyExists` | destination path | `rift directory already exists: {path}` |
| `missing_rift` | `MissingRift` | expected path | `cannot remove subtree while a recorded rift path is missing: {path}` |
| `inside_source` | `InsideSource` | destination path | `cannot copy a workspace into itself: {path}` |
| `invalid_config` | `InvalidConfig` | config file path | `invalid rift config at {path}: {message}` |
| `hook_failed` | `HookFailed` | destination workspace | `postcreate hook failed at {path}: \`{command}\` {message}` |

### FFI protocol codes

These codes are produced only inside the FFI crate and do not have a `rift::Error` counterpart.

| Code | When raised | Example `message` |
|------|-------------|-------------------|
| `invalid_request` | Null input pointer, invalid UTF-8, or JSON deserialization failure | `rift_ffi_call received a null request` |
| `panic` | Unwind inside `rift_ffi_call` | `rift FFI call panicked` |
| `serialization` | Response JSON serialization failure, or response contains an interior null byte | `failed to serialize response` / `response contained an interior null byte` |

## Path-bearing variants

Twelve core codes attach a `path` field in FFI and JavaScript. Use `error.path` for programmatic handling; read `error.message` for the full human-readable string.

| Code | What `path` identifies |
|------|------------------------|
| `initialization_required` | Source workspace that must be converted (btrfs subvolume init) before `create` |
| `workspace_not_initialized` | Directory where no `.rift` marker chain resolves to a registry record |
| `missing_marker` | Directory registered in SQLite but missing its `.rift` file |
| `unsupported_entry` | Non-file, non-directory, non-symlink entry encountered during copy |
| `not_managed` | Path referenced in ancestry walk with no registry parent |
| `marker_mismatch` | Directory whose `.rift` ID does not match the registry record at that path |
| `unknown_marker` | Directory with a `.rift` ID absent from the registry |
| `already_exists` | Destination rift path or trash relocation target that already exists |
| `missing_rift` | Registry-active child path missing on disk during remove |
| `inside_source` | Proposed child destination that falls inside the source tree |
| `invalid_config` | `.rift.toml` file path (typically `{workspace}/.rift.toml`) |
| `hook_failed` | Newly created child workspace where the hook ran (`RIFT_DESTINATION`) |

<Warning>
`hook_failed` and `invalid_config` expose the failing command and validation detail only in `message`, not as separate JSON fields. Parse `message` for display; branch on `code` and `path` in application logic.
</Warning>

## Common `message` details by code

### `invalid_path`

| Trigger | `message` detail |
|---------|------------------|
| Path is not a directory | `not a directory: {path}` |
| Invalid `--name` / `name` option | `invalid rift name: {name}` |
| Default database location unavailable | `user data directory is unavailable` |
| Relative `--into` resolution | `workspace has no parent`, `workspace has no name`, `rift has no parent`, `rift has no name` |
| Trash relocation | `trash path has no parent` |
| Null bytes in paths | `path contains a null byte: {path}` |
| btrfs ioctl name validation | `invalid btrfs subvolume name: {path}` |
| Registry UTF-8 constraint | `path is not valid UTF-8: {path}` |

### `cow_unavailable`

| Platform / context | Typical `message` detail |
|--------------------|--------------------------|
| Windows and other unsupported OS | `no copy-on-write strategy has been implemented for this platform` |
| Linux btrfs, wrong filesystem | `Linux snapshot creation requires btrfs; {path} is on another filesystem` |
| Linux btrfs `init` on non-btrfs | `{path} is not on a btrfs filesystem` |
| Linux reflink, cross-device | `Linux reflinks require source and destination on the same filesystem: {path}` |
| Linux reflink probe failure | `{path} does not support Linux copy-on-write reflinks: ...` |
| APFS `clonefile` failure | `failed to clone {path}: {os_error}` |
| Linux per-file reflink failure | `failed to reflink {path}: {os_error}` |
| btrfs snapshot/subvolume ioctl failure | `failed to {action} {path}: {os_error}` |
| btrfs init activation rollback | `failed to activate initialized workspace; restored the original workspace: ...` |

### `unsafe_git`

Raised by `git::check_source` before `init` or `create` when the source has a `.git` entry that is not a directory (linked worktree) or when any of these in-progress states exist:

`MERGE_HEAD`, `CHERRY_PICK_HEAD`, `REVERT_HEAD`, `BISECT_LOG`, `rebase-merge`, `rebase-apply`, `index.lock`, `HEAD.lock`

| Condition | `message` detail |
|-----------|------------------|
| Linked worktree `.git` file | `linked Git worktree sources are not supported` |
| In-progress Git operation | `Git state in progress: {state}` |

### `invalid_config`

Raised when `.rift.toml` exists but fails validation:

- TOML parse errors (serde message appended)
- `version` other than `1` → `unsupported config version {n}`
- Empty `hooks.postcreate[].run` → `postcreate run cannot be empty`
- Unknown fields (denied by schema) → serde unknown-field error

### `hook_failed`

Raised after the child workspace is registered when a postcreate hook exits non-zero or fails to start:

| Condition | `message` detail |
|-----------|------------------|
| Command cannot spawn | `` `{command}` failed to start: {error} `` |
| Non-zero exit | `` `{command}` exited with {status} `` |

Hook environment variables: `RIFT_SOURCE`, `RIFT_DESTINATION`, `RIFT_ID`, `RIFT_PARENT_ID`.

## CLI behavior

On failure the CLI writes the error message to **stderr** and exits with code **1**. Most codes print the core `Display` string unchanged.

Three workspace-state codes are rewritten for operator guidance:

| Code | CLI stderr message |
|------|-------------------|
| `initialization_required` | `this workspace must be initialized first; run \`rift init\` from its root folder` |
| `workspace_not_initialized` | `no initialized workspace found; run \`rift init\` from the root folder` |
| `missing_marker` | `this workspace is missing its \`.rift\` marker; run \`rift init\` to restore it` |

### CLI-only error (not a `RiftError` code)

`rift remove` on a root workspace without `-f` raises `CliError::ForceRequired` before calling the core library:

```
This is the root workspace.

Unregistering it removes Rift metadata and trashes all child rifts.
Run `rift remove -f` to continue.
```

This error has no `code` field and is not thrown by the JavaScript API.

## Errors by operation

| Operation | Codes commonly encountered |
|-----------|---------------------------|
| `init` | `cow_unavailable` (btrfs non-btrfs), `unsafe_git`, `marker_mismatch`, `io`, `walk` |
| `create` | `initialization_required` (btrfs unconverted source), `workspace_not_initialized`, `unsafe_git`, `inside_source`, `already_exists`, `cow_unavailable`, `unsupported_entry`, `invalid_config`, `hook_failed`, `invalid_path` |
| `remove` / `remove -f` | `missing_rift`, `marker_mismatch`, `workspace_not_initialized`, CLI `ForceRequired` |
| `remove --children` | Same as remove, plus `missing_rift` if a child path vanished |
| `list` / `ancestors` | `workspace_not_initialized`, `not_managed`, `marker_mismatch` |
| `gc` | `io`, `database`, `cow_unavailable` (btrfs subvolume deletion) |
| JavaScript `call()` | Any core code above, plus `invalid_request`, `panic`, `serialization` |

## Handling errors in JavaScript

<RequestExample>

```ts
import { create, RiftError } from "rift-snapshot"

try {
  const child = create({ from: "/path/to/source", name: "feature" })
} catch (error) {
  if (error instanceof RiftError) {
    switch (error.code) {
      case "initialization_required":
        // error.path is the source workspace
        break
      case "hook_failed":
        // error.path is the child workspace; message contains the command
        break
      default:
        console.error(error.code, error.message)
    }
  }
}
```

</RequestExample>

<Info>
Use an optional `database` option in API calls to point at a non-default SQLite registry—the same hidden `--database` flag the CLI accepts. Database open failures surface as `database` or `io` codes.
</Info>

## Related pages

<CardGroup>
  <Card title="Troubleshooting" href="/troubleshooting">
    Common failure modes and recovery steps for workspace, CoW, Git, and hook errors.
  </Card>
  <Card title="CLI reference" href="/cli-reference">
    Subcommands, flags, stderr behavior, and exit codes.
  </Card>
  <Card title="JavaScript API reference" href="/javascript-api">
    FFI request protocol, function signatures, and `RiftError` usage.
  </Card>
  <Card title="Postcreate hooks" href="/postcreate-hooks">
    Hook configuration, environment variables, and `hook_failed` failure behavior.
  </Card>
  <Card title="Copy strategies and platforms" href="/copy-strategies">
    Platform backends that produce `cow_unavailable` and `initialization_required`.
  </Card>
  <Card title="Git integration" href="/page-git-integration">
    Git detection rules and `unsafe_git` source-state checks.
  </Card>
</CardGroup>
