@unthrown/vitest ships six matchers. The Defect channel is the only one
without a value assertion:
| channel |
existence |
value |
Ok |
toBeOk |
toBeOkWith(value) |
Err |
toBeErr |
toBeErrWith(error), toBeErrTagged(tag, payload?) |
Defect |
toBeDefect |
— |
Asserting what caused a defect currently takes a two-step, and the narrowing
step only works on a synchronous Result — on an AsyncResult the value has to
be awaited separately, which is exactly what the matchers exist to avoid:
const r = await someOperation();
expect(r).toBeDefect();
if (r.isDefect()) expect(r.cause).toBeInstanceOf(TypeError);
Proposal
Add toBeDefectWith(cause), mirroring toBeErrWith
(packages/vitest/src/index.ts:270) exactly:
function toBeDefectWith(this: MatcherState, received: unknown, expected: unknown): MatcherResult {
const { stringify } = this.utils;
const { equals } = this;
return settle("toBeDefectWith", this, received, (result) => {
const pass = isDefect(result) && equals(result.cause, expected);
…
});
}
Going through settle means it inherits the thenable handling and the
forgotten-await net for free, like every other matcher.
Usage:
await expect(asyncResult).toBeDefectWith(expect.any(TypeError));
expect(result).toBeDefectWith(theOriginalCause);
expected is unknown, matching the channel: a defect's cause is unknown by
design, so there is no tighter type to give it and no tag-aware variant to add
(the toBeErrTagged counterpart has no meaning here).
Checklist
expect.extend registration and the raw-function export (index.ts:341-343).
- The
UnthrownMatchers type (index.ts:397-399).
CLAUDE.md — the vitest package description says "the six raw matcher
functions" and the status section enumerates them.
docs/how-to/test-with-vitest.md and the skills/unthrown/ copy, in the same
PR (the skill is hand-maintained and drifts).
- A changeset.
@unthrown/vitestships six matchers. TheDefectchannel is the only onewithout a value assertion:
OktoBeOktoBeOkWith(value)ErrtoBeErrtoBeErrWith(error),toBeErrTagged(tag, payload?)DefecttoBeDefectAsserting what caused a defect currently takes a two-step, and the narrowing
step only works on a synchronous
Result— on anAsyncResultthe value has tobe awaited separately, which is exactly what the matchers exist to avoid:
Proposal
Add
toBeDefectWith(cause), mirroringtoBeErrWith(
packages/vitest/src/index.ts:270) exactly:Going through
settlemeans it inherits the thenable handling and theforgotten-
awaitnet for free, like every other matcher.Usage:
expectedisunknown, matching the channel: a defect'scauseisunknownbydesign, so there is no tighter type to give it and no tag-aware variant to add
(the
toBeErrTaggedcounterpart has no meaning here).Checklist
expect.extendregistration and the raw-function export (index.ts:341-343).UnthrownMatcherstype (index.ts:397-399).CLAUDE.md— the vitest package description says "the six raw matcherfunctions" and the status section enumerates them.
docs/how-to/test-with-vitest.mdand theskills/unthrown/copy, in the samePR (the skill is hand-maintained and drifts).