Skip to content
Eugene Lazutkin edited this page Jun 27, 2026 · 2 revisions

API

tape-six-invariant exports a small, predicate-only surface. check is the default export and also a named export; everything else is named-only:

import check from 'tape-six-invariant'; // the common case

import {
  check,
  hasHost,
  setAbsentBehavior,
  inert,
  throwOnFail,
  warnOnFail,
  InvariantError
} from 'tape-six-invariant'; // when you need more than check

The library is zero-dependency and never imports tape-six. The two coordinate through a single global slot, Symbol.for('tape6.invariant.host.v1'), which tape-six installs at module load. See Home for the overview.

check

check(cond: unknown, message?: string | (() => string)): asserts cond;

Records an invariant.

  • cond — pass/fail verdict; a falsy value fails the invariant. Normalized with !! when reported.
  • message — failure message. May be a () => string thunk so an expensive message is built only when it is resolved.

Behavior:

  • A tape-six run is hosting invariants — the check materializes into a counted assertion on the current test, with source location pointing at the check() call site. This happens whenever the code is reached, directly or transitively, from a test body.
  • No host (production) — on a falsy cond, the configured absent behavior runs with (cond, resolvedMessage). A truthy cond is an early return: the only cost when off is one global-property read.

The TypeScript asserts cond signature narrows the condition for callers.

import check from 'tape-six-invariant';

export function transfer(from, to, amount) {
  check(amount > 0, 'amount must be positive');
  check(from.currency === to.currency, 'currencies must match');
}

To remove check() from release bundles entirely, see Stripping checks in builds.

Lazy messages

Pass a () => string thunk to build an expensive message only when it is needed:

check(list.every(valid), () => `invalid items: ${list.filter(x => !valid(x)).join(', ')}`);

hasHost

const hasHost: boolean;

Import-time snapshot of whether a tape-six host was installed when this module loaded. Reliable because tape-six sets the slot at module load and test files import tape-six first; in pure production the slot is absent and hasHost is false.

Use it to gate expensive pre-check computation — work not worth doing unless something is listening:

import {check, hasHost} from 'tape-six-invariant';

if (hasHost) check(expensiveToCompute(), 'invariant holds');

Correctness never depends on the snapshot — check() reads the live slot per call.

setAbsentBehavior

type AbsentBehavior = (ok: unknown, message?: string) => void;
function setAbsentBehavior(fn: AbsentBehavior): void;

Replaces the absent-path behavior — what a failing check does when no host is present. Pass a canned behavior or your own (ok, message) => void. A non-function argument resets to inert.

The setter is module-scoped: each copy of the library configures itself independently (only the host slot is global). Set it once at startup.

import {setAbsentBehavior, throwOnFail} from 'tape-six-invariant';

setAbsentBehavior(throwOnFail);

// or anything you like — log, sample, increment a metric:
setAbsentBehavior((ok, message) => {
  if (!ok) metrics.increment('invariant.failed', {message});
});

Canned behaviors

Exported functions to pass to setAbsentBehavior (not magic strings):

Behavior Effect on a failing check Use
inert (default) nothing ship invariants into prod at zero cost
throwOnFail throw InvariantError(message) fail-fast enforcement
warnOnFail console.warn non-fatal telemetry signal

Deferring to node:assert

There is deliberately no canned node:assert behavior: a synchronous check should not dynamically import a module, and keeping the import on the caller's side leaves this package zero-dependency. Deferring to node:assert (or any other assertion library) is a one-liner — bring your own import:

import assert from 'node:assert';

setAbsentBehavior((ok, message) => assert.ok(ok, message));

InvariantError

class InvariantError extends Error {
  constructor(message?: string);
}

Thrown by throwOnFail. name is 'InvariantError'; the default message is 'Invariant failed'. On V8 (Node, Deno, Bun) the constructor frame is dropped from the stack so a thrown error points at the failing check.

Coordination protocol

For the design of the global slot, the marker-based source location, and cross-realm behavior, see the project's ARCHITECTURE.md. In short:

  • tape-six installs globalThis[Symbol.for('tape6.invariant.host.v1')] = {version, report} at module load (||=, first copy wins).
  • report resolves the live current tester via tape-six's getTester() and forwards to Tester.reportAssertion(...).
  • A global (not a module variable) so duplicate library copies in a dependency graph share one coordination point. Each realm (worker, iframe, subprocess) has its own slot.

Clone this wiki locally