Skip to main content
Version: v1.11.0

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:

  • ErrEventEmitFailed wraps an EventBus.Publish failure that happens after the transaction commits. The commit stands; only the post-commit emit could not be delivered.
  • ErrListenerClosed — you called Listen after closing the inbound PostgreSQL LISTEN/NOTIFY listener.
  • ErrNoSubscription — you called Receive before 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
}
PredicateTrue whenTypical response
quark.IsUniqueViolation(err)A unique or primary-key constraint rejected the row409 Conflict, naming the field
quark.IsDeadlock(err)The engine picked this transaction as a deadlock victimRe-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.