shield
keep faults named.
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 { assert, family, kind, run } from "jsr:@perish/shield";// 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.