error handling rots differently: a thrown value loses its shape at the first catch, so a boundary sees an unknown and every handler re-guesses what went wrong. shield keeps native throw but gives each failure a name — a domain declares its fault vocabulary once, and that one declaration types both the throw site and every handler.

native throw no Result monad zero deps below harness

quickstart

shield is a jsr package, runtime-pure with no dependencies. a business brings its own schema (zod, or the zero-dep kind phantom shown here); shield consumes the inferred shape and never validates — validation, if you want it, is the business's own call.

import
import { assert, family, kind, run } from "jsr:@perish/shield";
land.ts
// land.ts - one domain declares its failure vocabulary once
export const fault = family("land", {
	dirty: kind<{ branch: string }>(),
	missing: kind<{ path: string }>(),
	ahead: kind<{ base: string; by: number }>(),
});

the spec is the single source of truth: kinds and their payload shapes flow into both the runtime factories and the type of every handler.

// throw at the disease, deep in the flow
assert(clean, () => fault.dirty({ branch }));
// dispose at the boundary - every kind, or the compiler stops you
await run(land).catch(fault.consume({
	dirty: (f) => io.fail(`dirty on ${f.meta.branch}`),
	missing: (f) => io.fail(`no path ${f.meta.path}`),
	ahead: (f) => io.fail(`behind by ${f.meta.by}`),
}));

one source of truth

family(name, spec) declares a domain's failure kinds once; the factories and every handler's meta type derive from it. no compound name is minted — the surface is a value on the domain module.

native throw, no monad

a fault propagates by being thrown, never wrapped in a Result. run absorbs every non-fault into a foreign kind, so a handler always faces a fault, and a bug still crashes loudly unless routed on purpose.

the boundary consumes

consume is exhaustive over a family's kinds — every kind handled, or the compiler stops you. add a kind and every boundary must reconsider. compose boundaries by chaining catch.

below harness

shield is a language substrate under harness: runtime-pure, no console, no runtime globals. adapters at the harness seam mint faults from native errors, so shield never touches the platform.