Skip to content
spockIt’s only logical !
Non-normative guide. This page shows how to use Spock; the v0 specification governs where they disagree. Spock is pre-1.0 and interfaces may change — see project status.

Error codes

Spock has two error planes, and they never mix. Runtime errors are codes a server emits on the wire — clients match on them, so they are API, frozen additively for v0.x. Compile-time diagnostics are codes spock check emits at the author — each one names a rule the program broke, with a source span. This page is the lookup table for both.

The runtime vocabulary

Every runtime code belongs to exactly one of three populations:

  • Derived errors are minted from schema constraints — team_name_taken from a unique field — and are never hand-written.
  • Reserved codes are protocol-owned — not_found, bad_request, and their peers — and mean the same thing in every contract.
  • Product errors are top-level error declarations a program authors, raised from fn bodies as refusals.

Experimental — RFD 0024 implementation preview. Top-level error declarations ship in toolchain 0.5.2 and later as an implementation preview. RFD 0024 remains a draft; this syntax is not yet part of the normative v0 specification.

The 0.5.2 preview replaced implicit refusal-code minting: a fn’s ! clause no longer creates a code by naming it. A program migrating from an earlier toolchain adds one top-level error declaration per authored refusal, and renames any refusal called unauthorized or conflict — those codes are storage-protocol-owned. The envelopes on the wire, the 409 refusal status, and rollback behavior are unchanged.

Derived errors

Schema-derived errors are part of the contract — visible at GET /~contract before any request is made. The v0 specification, §6.1, defines six derived kinds and one product-rule kind, refused:

KindDerived fromCodeREST status
keythe key<table>_already_exists409
uniqueeach unique field or group<table>_<fields joined by _>_taken409
requiredeach required field that can be absent on insert (no default) or cleared on update (non-key)<table>_<field>_required422
ref_not_foundeach reference field<table>_<field>_not_found422
restrictedany inbound restrict reference<table>_restricted409
invalideach closed-set field or check-validated field/group<table>_<fields joined by _>_invalid422
refuseda fn raising a declared product errorthe declared code409

The status split is principled. A 409 (key, unique, restricted, refused) is a conflict with existing state — the same payload could succeed against a different database. A 422 (required, ref_not_found, invalid) is intrinsic to the request itself, next to type_mismatch.

Watch the derivation on a concrete program:

table team {
key id: uuid = auto
name: text unique
}
table member {
key id: uuid = auto
email: text unique
team: team
access: "admin" | "editor" | "viewer"
}

spock build enumerates member’s codes in the contract — one per constraint, statuses included:

"errors": [
{ "code": "member_already_exists", "kind": "key", "fields": ["id"], "status": 409 },
{ "code": "member_email_taken", "kind": "unique", "fields": ["email"], "status": 409 },
{ "code": "member_email_required", "kind": "required", "fields": ["email"], "status": 422 },
{ "code": "member_team_required", "kind": "required", "fields": ["team"], "status": 422 },
{ "code": "member_access_required", "kind": "required", "fields": ["access"], "status": 422 },
{ "code": "member_team_not_found", "kind": "ref_not_found", "fields": ["team"], "status": 422 },
{ "code": "member_access_invalid", "kind": "invalid", "fields": ["access"], "status": 422 }
]

team additionally carries team_restricted, because member.team points at it with the default on delete restrict; member has no inbound references, so it gets no restricted code. id never derives a required error — it has a default.

Reserved codes

Reserved codes are not derived from any declaration and keep one spelling in every contract:

CodeStatusWhere it appears
not_found404a by-key read, update, or delete addressing a missing row; a fn declared -> t whose SQL matched no row
type_mismatch422a value that does not parse as the declared type — in a write body, a filter operand, or an rpc argument
unknown_field422a body or filter names a field the table does not declare
bad_request400malformed JSON, filter and paging violations, GET on a mut fn (as a 405), a foreign-key failure inside escape SQL
internal500the fn body broke its declared contract — wrong row arity, a NULL under a non-optional shape — or an unexpected engine failure
unauthorized401storage plane only: a missing, malformed, or expired signed-URL signature
conflict409storage plane only: a PUT to an object that is not pending

The first five are function-applicable: a fn may list them in its ! clause. The storage-only pair may not — those codes belong to the byte plane.

One collision-free vocabulary

Derived, reserved, and product codes share a single namespace, and the checker enforces it: two constraints deriving the same code is E044, two declarations of one product error is E051, and a product error claiming a derived or reserved spelling is E053. The collision below is real — the unique field region_code and the unique group (region, code) both derive account_region_code_taken, because the underscore-join is not injective:

table account {
key id: uuid = auto
region: text
code: text
region_code: text unique
unique (region, code)
}

The checker refuses the program rather than shipping an ambiguous code — codes are API, and an ambiguous one cannot be matched on.

Envelopes

REST carries every failure in one envelope, with the fields of the v0 specification, §8.1 — code, kind, table, fields, message:

{
"error": {
"code": "team_name_taken",
"kind": "unique",
"table": "team",
"fields": ["name"],
"message": "team.name is already taken"
}
}

GraphQL returns HTTP 200 and carries the same payload — minus the status, which its transport does not use — in errors[].extensions:

{
"data": null,
"errors": [
{
"message": "team.name is already taken",
"locations": [{ "line": 1, "column": 12 }],
"extensions": {
"code": "team_name_taken",
"kind": "unique",
"table": "team",
"fields": ["name"]
}
}
]
}

A refusal’s envelope has kind: "refused", table: null, and empty fields — the code itself is the payload. Statuses and the endpoint-level behavior live on the HTTP API page.

Compile-time diagnostics

Every diagnostic carries a stable code and a source span. E-codes are program errors from the checker; L-codes are lexical and parse errors. The v0 specification, §4, is normative; this table is a convenience mirror.

CodeCondition
E001duplicate table name
E002duplicate field name within a table
E003unknown type (not builtin, not a declared table)
E005table declares no key
E006table declares more than one key
E007composite key names an unknown field
E008key field (inline or composite member) is optional
E009default incompatible with the field type
E010reference target table has a composite key (v0 restriction)
E011unique group names an unknown field
E012table has no fields
E014duplicate field within a composite key or unique group
E015on delete on a non-reference field
E016default on a reference field (= me is the one carve-out, E047)
E017a table’s key type resolves through a reference cycle
E020seed row names an unknown table
E021seed row names an unknown field
E022seed row omits a required field that has no default
E023seed value incompatible with the field type
E024seed value references an unknown or not-yet-defined binding
E025seed binding’s table does not match the reference target
E026seed binding used as the value of a non-reference field
E027duplicate seed binding name
E028seed row sets the same field twice
E030duplicate record name
E031record name collides with a table (one type namespace)
E032record has no fields
E033table-only syntax in a record
E034record field is not a builtin scalar
E035duplicate fn name
E036fn parameter type unknown, a record, or a composite-key table
E037fn return shape is not a declared table or record
E038duplicate fn parameter name
E039duplicate error code in one fn ! clause
E040on delete set null on a required reference
E041check names a fn that does not exist
E042check validator law violated (not a read fn, not bool, not a single clauseless SELECT, non-deterministic, wrong arity, or attached to a closed-set/auto/now field)
E043closed-set type is degenerate (fewer than two values, a duplicate or empty value, or used as a key field)
E044two derived error codes collide
E045more than one auth table
E046the auth table’s key is not a single scalar column
E047= me is not on a reference to the auth table, or no anchor is declared
E048a user table is named storage_object (reserved for the storage builtin)
E049file("...") on a field that does not reference storage_object
E050a file("...") seed asset path is absolute or escapes the source directory
E051two top-level error declarations use the same identifier (RFD 0024 preview)
E052a code in a fn ! clause is not a declared product error, a schema-derived error, or a function-applicable reserved error (RFD 0024 preview)
E053a product-error declaration collides with a derived or reserved code (RFD 0024 preview)

The gaps in the numbering (E004, E013, E018, E019, E029) carry no diagnostic in v0; the shipped checker emits none of them.

CodeCondition
L001unexpected character
L002identifier contains an uppercase letter (identifiers are lowercase snake_case)
L003unterminated string
L004integer literal does not fit in a signed 64-bit integer
L005a keyword reserved for a future version used as an identifier
L006unterminated raw string
L010parse error — expected <X>, found <Y>
L011dangling doc comment: a /// that documents nothing
L012misplaced inner doc: a //! after the first declaration

L001 and L010 are the toolchain’s stray-character and unexpected-token codes; the specification defines the remaining lexical rules in §2 and the doc-comment placement rules in §4.