Errors
The full tagged-error catalog, covering engine errors, wire-only tags, and how each failure surfaces to a caller.
Every fallible call in Lakefront fails into one of two catalogs: engine errors, raised inside the engine and carried unchanged across the wire, and wire-only tags, synthesised by the transport or the server before a handler runs. This page lists both, with the fields and conditions behind each.
How failures surface
Engine methods and BoardClient’s mutation methods return Result<T, WireError> (from better-result): check isErr(), branch on error._tag, or call .unwrap() for the happy path.
const outcome = await board.insertMany(rows);
if (outcome.isErr()) {
console.error(outcome.error._tag, outcome.error.message);
} else {
console.log("committed at", outcome.value);
}
Inside engine.transaction(...), TransactionOps methods throw instead: the transaction absorbs the failure and rolls back, so there’s no Result for call sites inside the callback to forget to check.
The client’s builder reads (select, selectAll, db) also throw: Kysely’s Driver interface is exception-based, so LakefrontConnection.executeQuery is the one place a Result is deliberately unwrapped.
Underneath, transports raise RpcError (from lakefront/contract) for a non-200 response with no JSON envelope. createRpcClient’s retry loop always catches it internally and converts the final failure to a TransportFailed Result rather than letting it escape as a throw. See the client reference.
Engine errors
Raised by the engine; the same _tag crosses the wire unchanged as a WireError.
| Tag | Fields | When |
|---|---|---|
InvalidBoardId |
boardId, message |
A board id fails /^[A-Za-z0-9_]{1,64}$/u: board ids are interpolated into table names, so they’re validated rather than escaped. |
EntityNotAllowed |
entity, message |
A placeholder bound to an entity outside the server’s allowlist: the board-isolation boundary refusing. |
PlaceholderUnbound |
token, message |
A {{token}} placeholder either has an invalid name or reached execution still unbound. |
LakeUnavailable |
message, cause? |
The lake couldn’t be attached: catalog unreachable, or the concurrent first-boot race exhausted its retries. |
QueryFailed |
sql, message, cause? |
A query failed inside DuckDB. Carries the statement so the failure is diagnosable without re-running it. |
SyncFailed |
boardId, from, to, message, cause? |
Applying the change feed to a replica failed. The replica is a cache, so recovery is always rebuild, never repair. |
ReplicaUnavailable |
boardId, message, cause? |
A replica could not be materialised from the lake. |
WriteFailed |
boardId, message, cause? |
A write did not commit. Distinct from a read failure: it may have landed, so it must not be retried blindly. |
ReservedColumnName |
boardId, column, message |
A write named a column with the engine’s reserved _ prefix: those carry the version stamps that upsert ordering, compaction, and replica deletes all read. |
ColumnTypeConflict |
boardId, column, columnType, message, cause? |
A value can’t be stored in the column’s current type and the engine won’t reshape the board silently. For example, a string written into a column inferred as double. Fix with an explicit widenColumn(board, column, to), naming the lowercase ColumnType vocabulary (see the schema guide). |
BoardNotFound |
boardId, message |
The board doesn’t exist: reading one never written to, writing under autoCreate: "never", or touching one another node has since dropped. |
BoardExists |
boardId, message |
createBoard on a board that’s already there. |
UnknownColumn |
boardId, column, message |
A write named a column that doesn’t exist, under engine-wide columns: "declared" or a board created with strict: true. See createBoard in the engine reference. |
Wire-only tags
Never raised by the engine: synthesised at the client transport or the server’s request handling, before (or instead of) a handler running.
| Tag | HTTP status | When |
|---|---|---|
Unauthenticated |
401 |
Missing or invalid bearer token, when auth is on. |
MissingBoardEnvelope |
401 |
No x-lakefront-board header and the token doesn’t pin exactly one board. |
Forbidden |
403 |
Token’s claims don’t allow the procedure’s required verb on this board. |
InvalidInput |
400 |
Request body isn’t valid JSON, or fails the procedure’s input schema. |
UnknownProcedure |
404 |
<procedure> in /rpc/<procedure> isn’t in the contract. |
MalformedResponse |
— (client-side) | The response body didn’t decode into a valid envelope for the procedure that was called. |
TransportFailed |
— (client-side) | Every retry attempt failed at the transport level (network error, timeout, a non-JSON 5xx). See the retry policy in the wire contract. |
Full status/tag mapping and the envelope discipline that keeps a ran-but-failed handler at 200 are covered in the wire protocol reference.
messageOf
function messageOf(cause: unknown): string;
Normalises a thrown value into a message without swallowing its type: cause instanceof Error ? cause.message : String(cause). Exported from both lakefront/contract and lakefront (core/errors.ts re-exports the contract’s copy so engine code keeps one import path).
Fixing the actionable ones
ColumnTypeConflict: a value doesn’t fit the column’s current type. Fix withwidenColumn(board, column, to), using the lowercaseColumnTypevocabulary ("text","bigint","double", …). See the engine reference and schema guide.LakeUnavailable: the catalog Postgres or the lake storage is unreachable. See the troubleshooting guide.