Tolerance and error handling
Real-world tolerance
Production HL7 traffic routinely violates the published spec: trailing whitespace, MLLP framing, mixed-case segment names, unknown escape sequences, non-canonical timestamps. A parser that rejects those messages is useless on real integrations. Postel's Law applies: the parser is liberal, the emitter is conservative.
Every deviation the parser encounters is classified into one of four tiers:
| Tier | Behavior | When | Example codes |
|---|---|---|---|
| 0 | Silent | Spec-compliant input | (none) |
| 1 | Auto-handled | Trivial deviation, no warning | Trailing whitespace tidy |
| 2 | Warning | Recoverable deviation | MLLP_FRAMING_STRIPPED |
| 3 | Fatal (always) | Unrecoverable structural error | NO_MSH_SEGMENT |
Tier-2 warnings are plain data attached to msg.warnings. Every warning carries a stable string code, a human-readable message, and a position so you can programmatically react to specific deviations. In position, segmentIndex counts the message's segments from 0 (MSH is segment 0), while fieldIndex, repetitionIndex, componentIndex, and subcomponentIndex are 1-indexed HL7 positions:
import { parseHL7 } from "@cosyte/hl7";
const msg = parseHL7(raw);
for (const w of msg.warnings) {
console.log(`${w.code} at segment ${w.position.segmentIndex}: ${w.message}`);
}
The full list of Tier-2 codes lives in src/parser/warnings.ts. Narrow on w.code === WARNING_CODES.UNKNOWN_SEGMENT (and friends) for typo-free comparisons.
Need zero tolerance instead? parseHL7(raw, { strict: true }) escalates every Tier-2 deviation to a thrown Hl7ParseError. Use strict in CI validators; leave it off for production ingestion.
The 4 Tier-3 fatal codes (NO_MSH_SEGMENT, MSH_TOO_SHORT, INVALID_ENCODING_CHARACTERS, EMPTY_INPUT) always throw regardless of mode. They represent inputs the parser can't meaningfully recover from.
Error handling
The library throws exactly three error types, all exported from the package barrel.
Hl7ParseError
Thrown by parseHL7 when the input hits one of the 4 Tier-3 fatal codes (see above). Carries positional context plus a short snippet taken from the start of the input (up to 40 characters, plus an ellipsis when truncated).
import { parseHL7, Hl7ParseError, FATAL_CODES } from "@cosyte/hl7";
try {
parseHL7("");
} catch (err) {
if (err instanceof Hl7ParseError) {
console.log(err.code); // "EMPTY_INPUT"
console.log(err.position); // { segmentIndex: 0 }
console.log(err.snippet); // "" (first 40 chars of input, for logging)
// Compare codes through the FATAL_CODES registry rather than string literals:
if (err.code === FATAL_CODES.EMPTY_INPUT) {
// the input was empty or held only whitespace
}
}
}
Hl7ParseError.snippet may contain PHI when parsing real clinical messages. The library does NOT redact it. Redact it at your call site if compliance demands it.
snippet is the only field carrying input verbatim and unfiltered (capped at 40 characters plus an ellipsis, but not shape-checked in any way), so redact it first.
message is bounded but not absolutely content-free, which is worth stating because message is what a logger prints by default and what stack embeds. A token taken from the input is echoed only when it matches the form the spec defines for it: a three-character segment identifier, an MSH-9 type, an MSH-12 version, or a charset label the closed Table 0211 actually contains. Anything else becomes <withheld>. So a message cannot carry a field's value, but it can carry a residue of up to three characters when a malformed line happens to look like a segment identifier. The other shapes are narrower in practice than their patterns allow, because the library only ever feeds them registry-matched values; the patterns themselves admit more (a message type up to 26 characters, a version up to 14), which matters only if you construct warnings yourself.
Under { strict: true } an escalated Tier-2 warning is thrown as an Hl7ParseError carrying that same bounded message, plus a snippet of the first 40 characters of the input rather than of the deviation's own segment.
Hl7ParseWarning
Tier-2 deviations: plain data, not thrown. Accumulated on msg.warnings and delivered to any onWarning callback in parse order. See the iteration example in Real-World Tolerance.
import type { Hl7ParseWarning, WarningCode } from "@cosyte/hl7";
function label(w: Hl7ParseWarning): string {
const c: WarningCode = w.code;
switch (c) {
case "MLLP_FRAMING_STRIPPED":
return "framed input";
case "UNKNOWN_SEGMENT":
return `unknown: ${w.message}`;
default:
return c;
}
}
Use the WarningCode union + switch for exhaustive handling: the type system catches missing cases if you enable switch-exhaustiveness-check.
ProfileDefinitionError
Thrown by defineProfile() when the options are structurally invalid. Covers the four failure modes locked by the profile system:
import { defineProfile, ProfileDefinitionError } from "@cosyte/hl7";
try {
defineProfile({
name: "broken",
customSegments: {
AB: { fields: { foo: 1 } }, // bad: segment name must match /^Z[A-Z0-9]{2}$/ or similar
},
});
} catch (err) {
if (err instanceof ProfileDefinitionError) {
console.log(err.profileName); // "broken"
console.log(err.message); // actionable diagnostic
}
}
The four cases that throw ProfileDefinitionError:
- Missing or non-string
name: every profile must identify itself. - Malformed
customSegments: segment name must pass the Z-segment regex; each field entry must map to a positive 1-indexed integer; no duplicate field names across merged segments. - Unsupported
dateFormatstokens: tokens must be drawn fromSUPPORTED_DATE_TOKENS(re-exported from the package barrel for introspection). - Unknown option keys: the options bag is closed; typos throw with "did you mean" hints.
All three error types (and the FATAL_CODES / WARNING_CODES registries, and the SUPPORTED_DATE_TOKENS set) are top-level exports: no internal reaching required.