Skip to content
tsbouncerpreview
reference

Errors

The codes, what raises them, and which ones are a denial.

TypeScript
import { createAuthz, defineModel, defineType, permission, relation } from '@tsbouncer/tsbouncer';
import { memoryStore } from '@tsbouncer/in-memory';
import type { Authz, CheckRequest } from '@tsbouncer/tsbouncer';

const model = defineModel({
  types: {
    user: defineType({}),
    document: defineType({
      relations: { owner: relation(['user']) },
      permissions: { read: permission.or('owner') },
    }),
  },
});

const authz: Authz = createAuthz({ model, store: memoryStore() });
const request: CheckRequest = {
  subject: 'user:alice',
  permission: 'document.read',
  resource: 'document:1',
};

Every public error extends AuthorizationError and carries a stable code, so an application can branch on it without matching on messages.

TypeScript
import { isAuthorizationError } from '@tsbouncer/tsbouncer';

try {
  await authz.assert(request);
} catch (error) {
  if (isAuthorizationError(error)) {
    console.log(error.code, error.details);
  }
}

The codes

coderaised whenwhat it means for you
access_denieda check was deniedthe normal 403 path
invalid_referencea reference is malformed, or names a type the model does not declarea bug, or unvalidated input
invalid_tuplea write is rejected by model validationa bad grant — 400, not 403
invalid_modeldefineModel was given a model that does not hold togethera boot-time bug
evaluation_limita depth, node, or time budget ran outfail closed, and log it
store_errorthe store rejected a read or writea database problem, not a denial
cache_errora cache rejected a set, or a memo was misconfiguredan infrastructure problem or a boot-time bug — never served as an answer

The distinction that matters

TypeScript
function statusFor(error: unknown): number {
  if (isAuthorizationError(error)) {
    if (error.code === 'access_denied') return 403;
    if (error.code === 'invalid_tuple') return 400;
    return 403; // evaluation_limit and friends: still fails closed
  }
  return 500;
}

Failing closed

A condition that throws, a missing or mistyped context key, a condition the model no longer declares, an unresolvable reference, a cycle, and an exhausted budget all resolve to not allowed. Never allowed, and never thrown through to the caller from an evaluation.

evaluation_limit is the one worth alerting on. A denial because the graph said no and a denial because the graph gave up look identical to a caller and very different to you.

ModelDefinitionError

Thrown by defineModel for a model that does not hold together, at boot:

  • a type name that is not [a-z_][a-z0-9_]*
  • a condition registered under a different name than its definition
  • a relation and a permission sharing a name
  • a cross-type reference to a relation that does not exist
  • a cycle the validator can see

Some cycles are invisible to a model-level check — a through relation can loop in a way only a runtime walk finds. The evaluator carries an active set for those, so the walk terminates and denies.

TupleValidationError

Thrown at write time, which is the point:

  • a relation the type does not declare
  • a subject type the relation does not accept
  • bound condition parameters that are unknown or mistyped
  • context supplied without a condition

Catching these at grant rather than at check is the difference between a typo that took five seconds and a permission that silently denied for six months.

Next