Error Handling
Stringent draws a hard line between input errors (bad expressions — never throw, returned as data) and programmer errors (bad schemas, bad grammars, bypassed compile-time checks — always throw). Failures split into two domains, each using the representation built for it:
- Parse-time failures are stringent’s positioned diagnostics
(
StringentError, below). - Data-time failures — the values object not matching the schema — are
arktype’s ArkErrors: serializable, with per-field path attribution
(
flatByPath), produced byevaluate()’s values check and by compiled rules.
safeParse never throws
Section titled “safeParse never throws”Its error object is a discriminated union — narrow on code and each
variant’s fields are exact (no optional-field guessing):
type StringentError = | { code: "PARSE_ERROR"; message: string; position: number; expected: readonly string[]; found: string } | { code: "TYPE_MISMATCH"; message: string; position: number; expected: readonly string[]; found: string } | { code: "UNEXPECTED_INPUT"; message: string; position: number; found: string; expected?: readonly string[] } | { code: "INVALID_SCHEMA"; message: string; position: 0 };
if (!result.success && result.error.code === "PARSE_ERROR") { result.error.expected; // readonly string[] — required on this variant}The four codes:
PARSE_ERROR— no interpretation matched the input.TYPE_MISMATCH— an expression parsed, but a constraint rejected it (e.g. anumber-constrained slot saw astring, or an identifier wasn’t in the schema).UNEXPECTED_INPUT— a prefix parsed, but trailing input remains.INVALID_SCHEMA— the schema argument’s defs don’t compile. This is a programmer error (TypeScript normally catches it at compile time viatype.validate);positionis fixed at 0 since the error is not about the input.
Example messages:
Expected a number expression at position 2, got unknown ('zz' is not in the schema)Expected a number (type of 'left') expression at position 4, got stringExpected a number (overlapping 'left') expression at position 5, got stringUnexpected input at position 6: found "junk!!" (expected "*", "+" or "==")The (type of 'left') and (overlapping 'left') annotations appear when the
rejecting constraint is a
binding reference
— they name the earlier operand whose parsed type the candidate had to match.
How errors are ranked
Section titled “How errors are ranked”Backtracking parsers see many failures per input; most are noise. The parser
records the furthest token failure plus the furthest-reaching constraint
mismatch span. A mismatch wins when its span reaches both the stuck position
and the furthest token failure — otherwise it is backtracking noise (e.g. a
ternary probing whether 1 is boolean) and the token story wins.
What throws
Section titled “What throws”| Error | Thrown by | Meaning |
|---|---|---|
StringentParseError |
parse() / evaluate() / compile() |
Invalid input or an invalid schema: for parse/evaluate, the compile-time check was bypassed (cast, wide schema type); for compile, dynamic input simply failed to parse. Carries the same fields as StringentError (all four codes) |
EvaluationError |
evaluateAst / evaluate |
Undefined identifier or path at evaluation, a node without eval, or values that fail the schema (evaluate only) |
Compiled rules (parser.compile) never throw for data — they return
ArkErrors, with the failure attributed to the field you pass as
options.path:
const out = rule({ values: { password: "a", confirmPassword: "b" } });// out instanceof type.errors → "values.confirmPassword must be passwords to match"Grammar mistakes (duplicate names, invalid precedence, mixed tail shapes,
dangling binding references, unresolvable constraints, …) throw at
createParser time — see
construction-time validation.