Skip to main content

Documentation 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.

In sqlfu, you handle database errors by branching on a .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 that better-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.
if (error instanceof SqlfuError && error.kind === 'unique_violation') {
  return response.status(409).json({error: 'email already taken'});
}
That code works unchanged whether the client is better-sqlite3, node:sqlite, D1, or Turso.

SqlfuErrorKind

The complete set of kinds:
type SqlfuErrorKind =
  | 'syntax'                  // malformed SQL
  | 'missing_table'           // SQLite "no such table"
  | 'missing_column'          // SQLite "no such column"
  | 'unique_violation'        // unique or primary-key constraint
  | 'not_null_violation'
  | 'foreign_key_violation'
  | 'check_violation'
  | 'transient'               // SQLITE_BUSY / SQLITE_LOCKED families
  | 'unknown'                 // mapper didn't recognize; inspect .cause
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

KindWhen it is raised
syntaxThe SQL string is malformed — parse error before execution
missing_tableThe query references a table that doesn’t exist
missing_columnThe query references a column that doesn’t exist
unique_violationA 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_violationA NOT NULL constraint failed
foreign_key_violationA FOREIGN KEY constraint failed
check_violationA CHECK constraint failed
transientSQLITE_BUSY or SQLITE_LOCKED families — safe to retry
unknownThe mapper couldn’t classify the error; inspect .cause and file a bug

SqlfuError shape

kind
SqlfuErrorKind
required
The normalized discriminator. Branch on this in application code instead of matching driver-specific messages or codes.
query
SqlQuery
required
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.
system
string
required
The OTel db.system.name value stamped by the adapter (e.g. 'sqlite'). Useful for tagging in observability tools.
cause
unknown
required
The original driver error, byte-identical and untouched. Useful when you need adapter-specific fields (rawCode, errcode, resultCode) or when debugging a kind: 'unknown'.
message
string
required
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".
stack
string
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
import {SqlfuError} from 'sqlfu';

try {
  await client.run(createUser);
} catch (error) {
  if (error instanceof SqlfuError && error.kind === 'unique_violation') {
    return response.status(409).json({error: 'email already taken'});
  }
  throw error;
}
Catch only the kinds you intend to handle and rethrow everything else. Unhandled errors surface as unhandled exceptions rather than being swallowed silently.

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
import {instrument, SqlfuError} from 'sqlfu';

const client = instrument(
  baseClient,
  instrument.onError(({error}) => {
    if (error instanceof SqlfuError) {
      Sentry.captureException(error, {
        tags: {
          'db.error.kind': error.kind,
          'db.query.summary': error.query.name || 'sql',
          'db.system': error.system,
        },
      });
    }
  }),
);
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
catch (error) {
  if (error instanceof SqlfuError && error.kind === 'unknown') {
    console.error('unrecognized DB error, please file an issue', error.cause);
  }
}
If you see kind: 'unknown' in production, the right response is to file a bug against sqlfu with the driver name and the value of .cause. The error mapper is library-maintained and not user-overridable — users shouldn’t need to configure it.
If you need to distinguish primary-key violations from unique-constraint violations (they both map to 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 for better-sqlite3, silently breaks when you switch to @libsql/client (which reports the extended code on .cause.code), and breaks again for node:sqlite (which uses a numeric errcode).
  • error.kind === 'unique_violation' works everywhere, now and when you change drivers.
Carrying .query and .system on the error itself also means a plain catch block can tag query names and systems without any QueryExecutionContext plumbing.

References

Build docs developers (and LLMs) love