The Defect Channel
A defect is a failure you did not model — a thrown TypeError, an un-triaged promise rejection, a bug in a callback. It is the third runtime state of a Result, and it is invisible to the type: it never appears in E.
This is the idea that sets unthrown apart. A defect is a value (not a thrown exception), so errors-as-values stays uniform — but it behaves very differently from a modeled Err.
Throw → defect
Any value thrown by a callback inside a combinator is caught and converted to a defect, never allowed to escape:
const r = ok(1).map(() => {
throw new Error("boom");
});
r.isDefect(); // trueThis is what makes "no try/catch at the edge" real: a bug in a .map becomes a defect, short-circuits the pipeline, and is handled once at match.
A defect flows through almost everything
A defect passes through every method untouched — except match and recoverDefect. The success and error combinators never see it:
const d = ok(1).map(() => {
throw boom;
});
d.map((n) => n + 1); // still a Defect — callback skipped
d.mapErr((e) => e); // still a Defect — callback skipped
d.recover(() => 0); // still a Defect — see belowThat last line is the crucial one.
recover clears the error channel, not the runtime
recover turns an Err into an Ok, so its type is Result<T | U, never>. But never describes only the error channel — a defect can still be present at runtime:
const recovered = d.recover(() => 99);
// type: Result<number, never>
recovered.isDefect(); // true — `never` does NOT mean "total"A defect is a bug; you should not be able to accidentally "recover" it into a success. So the recovering eliminators rethrow on a defect:
d.unwrapOr(0); // throws the original cause
d.getOrNull(); // throws the original cause
d.unwrapOrElse(() => 0); // throws the original causeThey recover a modeled Err, never an unmodeled defect.
unwrap is asymmetric
- On an
Err,unwrap()throws anUnwrapErrorcarrying yourE. - On a
Defect,unwrap()rethrows the original cause with its original stack — so an unhandled defect reaches the global handler looking like the real failure, not wrapped in library noise.
try {
d.unwrap();
} catch (e) {
e === boom; // true — same instance, original stack
}The only door: recoverDefect
When you genuinely need to handle a defect — say, to convert a third-party library's thrown error back into a modeled one — use recoverDefect. It is the only combinator that can observe a defect, and it re-enters the modeled world by returning a Result:
d.recoverDefect((cause) => (cause instanceof RangeError ? err("out_of_range") : err("unknown")));Use tapDefect to observe a defect's cause (e.g. logging) without changing it.
Recovering a defect should feel awkward — usually you don't. You let it bubble to the edge, log it, and return a 500.
→ Continue to Boundaries & Qualification.