In sqlfu, you handle database errors by branching on aDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/iterate/sqlfu/llms.txt
Use this file to discover all available pages before exploring further.
.kind discriminator — not by string-matching driver messages or comparing numeric result codes. Every error from every adapter is a SqlfuError with a normalized kind field that means the same thing regardless of which driver threw it.
Why normalized errors matter
The code that cares about a database error rarely cares which driver threw it. “That email is already in use” is a product outcome — a 409 response, or a validation message in the UI. The surrounding code shouldn’t need to know thatbetter-sqlite3 reports SQLITE_CONSTRAINT_UNIQUE, node:sqlite uses a numeric errcode: 2067, and @libsql/client wraps both behind a LibsqlError.
SqlfuError.kind closes that gap. You branch on the discriminator; sqlfu does the per-adapter mapping.
better-sqlite3, node:sqlite, D1, or Turso.
SqlfuErrorKind
The complete set of kinds:
Kind names are SQLSTATE-aligned with PostgreSQL’s error code naming convention, so that when a Postgres adapter lands, the mapping becomes a direct lookup rather than a second vocabulary.
missing_table and missing_column are the two deliberate deviations: SQLSTATE’s undefined_table and undefined_column collide with TypeScript’s undefined at reading time.Kind reference
| Kind | When it is raised |
|---|---|
syntax | The SQL string is malformed — parse error before execution |
missing_table | The query references a table that doesn’t exist |
missing_column | The query references a column that doesn’t exist |
unique_violation | A UNIQUE or PRIMARY KEY constraint failed. Primary-key violations collapse into this kind — from a product perspective both mean “that row already exists” |
not_null_violation | A NOT NULL constraint failed |
foreign_key_violation | A FOREIGN KEY constraint failed |
check_violation | A CHECK constraint failed |
transient | SQLITE_BUSY or SQLITE_LOCKED families — safe to retry |
unknown | The mapper couldn’t classify the error; inspect .cause and file a bug |
SqlfuError shape
The normalized discriminator. Branch on this in application code instead of matching driver-specific messages or codes.
The query that failed, as a
{sql, args, name?} object. .name is set when the query came from a named .sql file, making it useful for tagging errors in logs and APM tools.The OTel
db.system.name value stamped by the adapter (e.g. 'sqlite'). Useful for tagging in observability tools.The original driver error, byte-identical and untouched. Useful when you need adapter-specific fields (
rawCode, errcode, resultCode) or when debugging a kind: 'unknown'.Comes straight from the driver error.
console.error and error-reporting tools like Sentry will show the original signal text, such as "UNIQUE constraint failed: users.email".Preserved from the driver error. Your call-site frame is the first useful frame — stack traces point at where the query was issued, not at sqlfu internals.
Handling errors in application code
try/catch in application code
Handling errors in hooks
Because.query and .system live on the error itself, a plain onError hook doesn’t need a parallel context object:
instrument.onError with Sentry
kind is a natural low-cardinality dimension for Sentry, PostHog, or Datadog: specific enough to distinguish a constraint violation from a transient lock, broad enough not to explode your tag index.
Working with .cause
.cause holds the driver’s original error verbatim. It’s useful for the long tail: adapter-specific flags, nested wrapping, or debugging a kind: 'unknown'.
inspecting .cause for unknown errors
unique_violation), inspect .cause.code or .cause.rawCode for the adapter-specific extended code.
Why sqlfu wraps instead of rethrowing
Branching on.kind is stable across adapters. By contrast:
error.code === 'SQLITE_CONSTRAINT_UNIQUE'works forbetter-sqlite3, silently breaks when you switch to@libsql/client(which reports the extended code on.cause.code), and breaks again fornode:sqlite(which uses a numericerrcode).error.kind === 'unique_violation'works everywhere, now and when you change drivers.
.query and .system on the error itself also means a plain catch block can tag query names and systems without any QueryExecutionContext plumbing.
References
- SQLite result codes
- PostgreSQL SQLSTATE codes — context for the naming convention
- Observability — how
onErrorcomposes with OpenTelemetry, Sentry, and PostHog