13 · Error handling for hosts
Scenario
Your app calls convert(templateBuffer, dataBuffer). The template has
a typo, or the data is missing a required column. Now what?
xl3 raises structured errors (per ADR-0015) with a stable
error.code string. Hosts dispatch on the code — for localization,
retry logic, or operator-friendly messages.
Catch + dispatch
import { convert, isXtlError } from '@xl3-lang/xl3';
try {
const outputs = await convert(templateBuffer, dataBuffer, options);
// ship outputs
} catch (err) {
if (isXtlError(err)) {
switch (err.code) {
case 'xl3/source/missing-header':
return showOperator('Your data file is missing required columns.', err.message);
case 'xl3/inputs/missing-required':
return promptForMissingInput(err.message);
case 'xl3/filename/collision':
return showOperator('Two output files would have the same name. Check your data.', err.message);
default:
return showOperator('Conversion failed.', err.message);
}
}
// Non-xl3 error: probably a system fault. Re-throw.
throw err;
}
isXtlError(value) returns true only for Error instances whose
code starts with xl3/. Plain Error or DOMException etc. won't
match.
The error code catalog
Stable. Append-only. Renames are breaking changes per ADR-0015. The current set:
xl3/cell/*— cell-level failures (formula-no-cache,numfmt-coercion,row-outside-repeat)xl3/eval/*— expression evaluation (arity-mismatch,operand-coercion,unsupported-syntax)xl3/config/*—__config__issuesxl3/inputs/*— runtime input failuresxl3/source/*— source data issues (missing headers, undeclared sources, reserved column names)xl3/sources/*—__sources__sheet issuesxl3/sheet/*— sheet-name issuesxl3/directive/*— directive syntaxxl3/join/*—@joinclause issuesxl3/xlookup/*—XLOOKUPfailuresxl3/filename/*— output filename issuesxl3/parser/*— parser failuresxl3/lists/*—__lists__reference issuesxl3/expression/*— expression-level rejections (bracket-outside-block,unknown-name)xl3/block/*— data-block layout (overlap,empty-table)xl3/group/*—@groupissues (missing-key)xl3/subtotal/*—@subtotalissues (outside-group,bad-aggregate,mixed-row,explicit-block-unsupported)xl3/source-json/*— JSON source input (invalid) — reachable viaconvertJson,previewJson, andvalidateSourceJson
Full list in impl/js/src/error-codes.ts.
Common cases worth handling explicitly
Missing required input (xl3/inputs/missing-required):
The template declares an input as required: true and the host didn't
supply it. Show a form, ask the operator, retry.
Filename collision (xl3/filename/collision):
Two distinct file group keys sanitized to the same filename
(e.g., Seoul/Korea and Seoul:Korea both → Seoul_Korea.xlsx). The
operator usually needs to clean their data, not the template.
Source mismatch in XLOOKUP (xl3/xlookup/source-mismatch):
The template author wrote XLOOKUP(x, A[k], B[v]) where A and B
are different sources. Template needs fixing — not an operator issue.
No match in XLOOKUP (xl3/xlookup/no-match):
The lookup value isn't in the lookup column. Either the operator's
data is incomplete, or the template should use @join instead (drops
unmatched rows).
Locale
error.message is English. To localize, dispatch on error.code in
your host and provide your own messages — do NOT translate the
engine's English strings. The English text is part of the conformance
contract (fixtures match on substrings of it).
Validate before convert
For a compatibility gate, call validateSource(template, data) or
validateSourceJson(template, sourceJson) first. Validation returns every
source/schema problem it can name, instead of throwing on the first one, and it
does not render workbooks.
import { validateSource } from '@xl3-lang/xl3';
const report = await validateSource(templateBuffer, dataBuffer);
if (!report.ok) {
return showOperator(
'Your data file does not match this template.',
report.diagnostics,
);
}
The default { depth: 'schema' } is the cheap preflight: declarations and
headers only. Use { depth: 'full' } before a production conversion when row
values may be malformed. Full depth also catches XLSX formula cells without a
cached result and JSON row-width/tagged-value/date errors, while still avoiding
rendering:
const report = await validateSourceJson(templateBuffer, sourceJson, {
depth: 'full',
});
validateSource*() is stricter than the legacy preview warning path: if the
template reads {{ [Amount] }} and the source has no Amount header, the
validator reports xl3/source/unknown-column as an error. That makes
report.ok === true mean the template's required source columns are present.
The same gate is available without embedding JavaScript:
xl3 validate template.xlsx --data=data.xlsx
xl3 validate template.xlsx --data=source.json --json
xl3 validate template.xlsx --data=source.json --depth=full --json
cat source.json | xl3 validate template.xlsx --data=- --json
The command exits 0 when the source is compatible, 1 when validation
reports errors, and 2 for invalid CLI usage. With --json, the complete
ValidationReport is written to stdout even when the exit code is 1, so CI
and operator tooling can display every diagnostic from a single run.
After validation, use preview(template, data, options) when the host needs
planned filenames, sheet names, row counts, resolved inputs, or non-fatal
warnings:
const preview = await xl3.preview(templateBuffer, dataBuffer, options);
// preview.warnings: non-fatal issues
// preview.inputs: resolved input values (after defaults + coercion)
// preview.files / preview.sources: what convert() would produce
preview() keeps xl3w/parser/missing-column warnings for backwards
compatibility. New "Validate" buttons should prefer validateSource*().
Spec pointers
- ADR-0015 — Structured error reporting.
- ADR-0078 — Source compatibility validation API.
spec/evaluation.md"Errors".- Cookbook 06 for input-related errors.