Fabriq
Reference

Errors Reference

Fabriq's typed and sentinel errors, when each occurs, and how callers detect them.

Fabriq exposes a typed error taxonomy. Sentinel values are matched with errors.Is; rich struct variants carry detail, satisfy errors.As, and still match their sentinel through a custom Is method.

The canonical values live in core packages (core/fabriqerr, core/tenant) — the root fabriq package imports core, never the reverse — and are aliased at the root so application code uniformly writes fabriq.ErrX and fabriq.XError.

Sentinel errors

Match these with errors.Is. They survive wrapping (fmt.Errorf("...: %w", err)).

ErrorRoot alias ofMessageWhen it occurs
ErrNoTenanttenant.ErrNoTenantfabriq: no tenant in contextAny fabric call whose context was not stamped with a tenant by auth middleware.
ErrTenantHookTrippedtenant.ErrTenantHookTrippedfabriq: tenant guard trippedThe grove pre-query/raw-SQL backstop detects a pool-path relational query (or unguarded tag_readings access) without a tenant predicate. Its firing indicates a bug in Fabriq — structural stamping was bypassed.
ErrNotFoundfabriqerr.ErrNotFoundfabriq: not foundAn aggregate or row does not exist within the calling tenant's scope.
ErrVersionConflictfabriqerr.ErrVersionConflictfabriq: version conflictA command's expected version does not match the stored aggregate version (optimistic concurrency).
ErrProjectionLagfabriqerr.ErrProjectionLagfabriq: projection laggingWaitForProjection deadline expires before the projection catches up to the requested version.
ErrStoreNotConfiguredfabriqerr.ErrStoreNotConfiguredfabriq: store not configuredA capability port whose backing store was not configured on Open is invoked (e.g. Graph() without FalkorDB, CatchUp without a tailer).
delta, err := f.WaitForProjection(ctx, "search", "asset", id, 7)
if errors.Is(err, fabriq.ErrProjectionLag) {
	// deadline expired before the projection reached v7
}

if errors.Is(err, fabriq.ErrStoreNotConfigured) {
	// the backing store for this port was not opened
}

ErrTenantHookTripped firing in production is a defect signal, not an expected control-flow error: it means a query reached the engine without the structural tenant stamping. It is counted as a metric trip and should be alerted on. See ADR 0002.

Struct errors

These carry fields. Recover them with errors.As; they also match their sentinel with errors.Is via a custom Is method. Both are aliased at the root (fabriq.VersionConflictError, fabriq.NotFoundError).

VersionConflictError

Reports an optimistic-concurrency failure. Matches errors.Is(err, ErrVersionConflict).

FieldTypeMeaning
AggregatestringAggregate/entity name.
AggIDstringAggregate instance ID.
Expectedint64Version the command expected.
Actualint64Version actually stored.

Message format: fabriq: version conflict on <Aggregate>/<AggID>: expected <Expected>, actual <Actual>.

_, err := f.Exec(ctx, cmd)
var vc *fabriq.VersionConflictError
if errors.As(err, &vc) {
	// vc.Expected, vc.Actual carry the mismatch — reload and retry
	log.Printf("conflict on %s/%s: had v%d, wanted v%d",
		vc.Aggregate, vc.AggID, vc.Actual, vc.Expected)
}
// Still matches the sentinel, even when wrapped:
if errors.Is(err, fabriq.ErrVersionConflict) { /* ... */ }

NotFoundError

Reports a missing aggregate within the tenant's scope. Matches errors.Is(err, ErrNotFound).

FieldTypeMeaning
EntitystringEntity name.
IDstringMissing instance ID.

Message format: fabriq: <Entity> "<ID>" not found.

var nf *fabriq.NotFoundError
if errors.As(err, &nf) {
	// nf.Entity, nf.ID identify what was missing
}
if errors.Is(err, fabriq.ErrNotFound) { /* ... */ }

Detection summary

ErrorDetect with
ErrNoTenanterrors.Is
ErrTenantHookTrippederrors.Is
ErrNotFounderrors.Is; errors.As(&*NotFoundError) for fields
ErrVersionConflicterrors.Is; errors.As(&*VersionConflictError) for fields
ErrProjectionLagerrors.Is
ErrStoreNotConfigurederrors.Is
VersionConflictErrorerrors.As; errors.Is(err, ErrVersionConflict)
NotFoundErrorerrors.As; errors.Is(err, ErrNotFound)

The sentinels are guaranteed distinct: errors.Is(a, b) is false for any two different sentinels. A wrapped VersionConflictError still satisfies errors.Is(err, ErrVersionConflict) and errors.As recovers the struct with its fields intact.

On this page