Errors API Reference
Quark never returns a bare string error you have to pattern-match. Every failure
mode it knows about wraps a package-level sentinel, so you branch with
errors.Is and keep the original message for logs. This page lists the
sentinels and explains, one by one, exactly what causes the non-obvious ones.
Sentinel Errors
var (
ErrNotFound = errors.New("record not found")
ErrInvalidModel = errors.New("invalid model")
ErrInvalidQuery = errors.New("invalid query")
ErrInvalidIdentifier = errors.New("invalid identifier")
ErrInvalidJSONPath = errors.New("invalid JSON path")
ErrInvalidJoin = errors.New("invalid JOIN ON clause")
ErrStaleEntity = errors.New("stale entity (optimistic-locking conflict)")
ErrUnsupportedFeature = errors.New("feature not supported by dialect")
ErrInvalidTimezone = errors.New("invalid column timezone")
ErrDialectNotSupported = errors.New("dialect not supported")
ErrConnection = errors.New("database connection error")
ErrTimeout = errors.New("query timeout")
ErrConstraintViolation = errors.New("constraint violation")
// Event bus + inbound LISTEN/NOTIFY listener
ErrEventEmitFailed = errors.New("event emit failed after commit")
ErrListenerClosed = errors.New("event listener closed")
ErrNoSubscription = errors.New("event listener has no channel subscribed")
)
ErrInvalidIdentifier
A table or column name was rejected before any SQL was assembled. It covers
every identifier you can pass in: a Where / OrderBy / GroupBy column, a
table name, a CTE name, an event channel, a migration target.
To be accepted, a name must match ^[a-zA-Z_][a-zA-Z0-9_]*$, be at most 64
characters, and not be a reserved SQL keyword. Identifiers cannot be bound as
parameters, which is why they are checked instead: a rejected one never reaches
the database. The sentinel is wrapped at the point of rejection, so
errors.Is(err, quark.ErrInvalidIdentifier) holds no matter which call site
produced it.
ErrInvalidJSONPath
WhereJSON received a path outside its
dotted-identifier grammar: ^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*$,
up to 256 characters. Quark binds the path as a parameter on every dialect, so a
malformed one is not an injection risk — it is rejected at the API surface so you
get a clear error instead of surprising SQL.
ErrUnsupportedFeature
The active dialect does not implement what the builder asked for — for example
ForUpdate on SQLite, or NoWait
on SQL Server. Quark returns this rather than emitting SQL the engine will
reject.
The message names the dialect and the specific gap, so you can branch by engine or fall back to another strategy — dropping down to a transaction-level lock, for instance.
ErrStaleEntity
An Update, UpdateFields, or Tracked.Save on a model with a
quark:"version" field tried to write a row that had already moved on. The
version predicate did not match, so no rows were written. Reload and replay, or
surface the conflict to the caller. See
Optimistic Locking for the tag
contract and a retry example.
ErrInvalidJoin
The ON clause of a Join / LeftJoin / RightJoin
fell outside the identifier-only grammar Quark validates at execution time. This
applies to both .On(left, op, right) and .OnRaw(clause).
The grammar accepts identifier-to-identifier comparisons (a.b = c.d), joined by
AND / OR, using the operators =, !=, <>, <, <=, >, >=. Literals,
function calls, subqueries, and parentheses are rejected. Prefer the structured
.On(...) form; .OnRaw is the escape hatch for clauses that don't fit the
simple binary shape.
ErrInvalidTimezone
A model field carries a quark:"tz=..." tag whose value is not a valid IANA
timezone name. Client.RegisterModel and Client.Migrate reject the model
immediately rather than failing later on the first query that binds or scans the
column, and the wrapped error names the field, the column, and the offending
string. See Timezones for the per-column
contract.
Event errors
Three sentinels come from the event subsystem:
ErrEventEmitFailedwraps anEventBus.Publishfailure that happens after the transaction commits. The commit stands; only the post-commit emit could not be delivered.ErrListenerClosed— you calledListenafter closing the inbound PostgreSQLLISTEN/NOTIFYlistener.ErrNoSubscription— you calledReceivebefore subscribing to any channel.
See Events for the bus and listener contracts.
Error Checking
user, err := quark.For[User](ctx, client).Find(id)
if errors.Is(err, quark.ErrNotFound) {
return nil, fmt.Errorf("user %d not found", id)
}
if errors.Is(err, quark.ErrTimeout) {
// Retry with backoff
}
if errors.Is(err, quark.ErrConstraintViolation) {
// Handle duplicate key, FK violation, etc.
}
Classifying a failure
ErrConstraintViolation tells you the database rejected the write, but not
which rule it broke — unique, foreign key, not-null and check violations all
arrive as the same sentinel. When the answer changes what you do, two
predicates give you the specific one:
if err := quark.For[User](ctx, client).Create(u); err != nil {
if quark.IsUniqueViolation(err) {
http.Error(w, "email already registered", http.StatusConflict)
return
}
return err
}
| Predicate | True when | Typical response |
|---|---|---|
quark.IsUniqueViolation(err) | A unique or primary-key constraint rejected the row | 409 Conflict, naming the field |
quark.IsDeadlock(err) | The engine picked this transaction as a deadlock victim | Re-run the transaction |
Both work on every supported engine and on both PostgreSQL drivers, and both match on the error code the driver reports rather than on the text of the message. That distinction matters more than it looks: a database server configured for a non-English locale translates its messages, so any check written against English wording stops recognising the very errors it was meant to catch. Code matching is unaffected by the server's locale and by wording changes between driver releases.
Both also walk the wrapping chain, so an error you have wrapped yourself still classifies.
IsDeadlock is for callers driving their own retry loop. If you build the
client with WithDeadlockRetry, quark already
re-runs the victim transaction for you and you will not see the error at all.
SQLite never reports a deadlock — it serialises writes — so the predicate is
always false there.
Error Wrapping
Quark wraps database errors with context:
// Timeout errors wrap context.DeadlineExceeded
if errors.Is(err, context.DeadlineExceeded) {
// Detected as ErrTimeout
}
Unique violations reach ErrConstraintViolation through the driver's error
code, the same route IsUniqueViolation uses. The remaining constraint kinds
— foreign key, not-null, check — are still recognised by matching the message
text each engine produces, which means they can go unclassified on a server
running in another language. Prefer IsUniqueViolation where the distinction
matters; the sentinel remains the broad "the database rejected this" signal.