Skip to main content
Version: v0.0.2

Tolerance tiers & warning codes

@cosyte/x12 follows Postel's Law: liberal on input, conservative on output. Real payer and clearinghouse traffic is full of deviations that a strict validator would reject and a production integration cannot afford to drop. The parser tolerates them, records each one as a stable-coded warning with positional context, and preserves the raw value, so you decide what to do, instead of the library throwing your pipeline off the rails on a vendor quirk.

The three tiers

  • Tier 1: spec-clean. Parses with no warnings.
  • Tier 2: tolerated deviation. A miscount, a dangling release character, an unknown CARC/RARC/HI code, an HL parent mismatch, a balance mismatch, a pre-005010 version. The parser keeps going, preserves the verbatim value, and emits a warning. This is the overwhelming majority of real-world quirks.
  • Tier 3: unrecoverable structural corruption. The bytes are not X12 at all. There are exactly four of these, and they always throw (see below).

Only Tier 3 throws by default. Everything in Tier 2 is a warning you triage, not an exception you catch.

Warnings collect on the model, and stream

Every warning lands on the returned model's .warnings array (ix.warnings, remit.warnings, sub.warnings, …), each carrying a stable code (from WARNING_CODES), a bounded, PHI-free message (it never echoes names, IDs, or dates), and a position. You can also stream them live via the onWarning callback:

import { parseX12, type X12ParseWarning } from "@cosyte/x12";

const seen: X12ParseWarning[] = [];

// ISA-12 declares an old version family (004010): tolerated, flagged, not fatal.
const raw =
"ISA*00* *00* *ZZ*SENDER *ZZ*RECEIVER " +
"*260601*1200*^*00401*000000001*0*P*:~" +
"IEA*0*000000001~";

const ix = parseX12(raw, { onWarning: (w) => seen.push(w) });

seen.length > 0; // => true
ix.warnings.length > 0; // => true

The four fatal codes

These are the only conditions that throw an X12ParseError, regardless of strict. They are unrecoverable structural corruption, not tolerable quirks:

import { FATAL_CODES } from "@cosyte/x12";

FATAL_CODES.X12_EMPTY_INPUT; // => "X12_EMPTY_INPUT"
FATAL_CODES.X12_NO_ISA_HEADER; // => "X12_NO_ISA_HEADER"
FATAL_CODES.X12_ISA_TOO_SHORT; // => "X12_ISA_TOO_SHORT"
FATAL_CODES.X12_INVALID_DELIMITERS; // => "X12_INVALID_DELIMITERS"
  • X12_EMPTY_INPUT: nothing to parse.
  • X12_NO_ISA_HEADER: the input does not begin with an ISA; it is not an X12 interchange.
  • X12_ISA_TOO_SHORT: the ISA is truncated below its fixed 106 bytes, so delimiters can't be read.
  • X12_INVALID_DELIMITERS: the delimiters can't be recovered from the ISA.
import { parseX12 } from "@cosyte/x12";

parseX12(""); // throws X12ParseError (X12_EMPTY_INPUT)

Escalate to strict when you want a conformance gate

For a trusted trading partner where any deviation should be a hard failure, pass { strict: true }. Every Tier-2 deviation then throws an X12ParseError carrying the same warning code it would otherwise have recorded, so a strict run is a spec-conformance gate, and a lenient run is production tolerance, with one code vocabulary across both:

import { parseX12 } from "@cosyte/x12";

const raw =
"ISA*00* *00* *ZZ*SENDER *ZZ*RECEIVER " +
"*260601*1200*^*00401*000000001*0*P*:~" +
"IEA*0*000000001~";

// The pre-005010 version is a warning by default; strict turns it into a throw.
parseX12(raw, { strict: true }); // throws X12ParseError (X12_PRE_005010)

Integrity checks warn: they never rebalance or renumber

The 835 balance invariants, the 837 HL parent-pointer integrity, and the serializer's envelope-count reconciliation all follow the same rule: on a mismatch they surface a warning and preserve the inbound values verbatim. The library will not "fix" a payer artifact for you. Gate your own posting/adjudication on the warning. See Decimal-exact money for the 835 balance equations, and Troubleshooting for the full symptom → cause table.