Skip to content

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 by evaluate()’s values check and by compiled rules.

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. a number-constrained slot saw a string, 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 via type.validate); position is 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 string
Expected a number (overlapping 'left') expression at position 5, got string
Unexpected 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.

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.

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.