Graceful Expression Parse Errors
DL#165 — Graceful Expression Parse Errors
Written for AI agents. See Log Methodology Note below for details.
Background
When a jay-html template contains an invalid expression — unsupported operator, malformed binding, typo — the PEG parser throws an error. This error propagates up through the compiler and crashes the page with a Vite plugin error.
Current behavior
[plugin:jay:runtime] Failed to parse expression [currentPath ^= '/docs/designer'].
Parse error: Expected "!=", "!==", "&&", ".", "<", "<=", "==", "===", ">", ">=", or "||" but "^" found.
The entire page fails to render. In dev mode this blocks the designer from seeing any output. In the AIditor workflow, this is especially problematic — the design tool may generate expressions using syntax the framework doesn't yet support, and a single bad expression takes down the whole page.
Where errors originate
All expression parsing flows through doParse() in expression-compiler.ts (line 220):
function doParse(expression: string, startRule: string, vars?: Variables) {
try {
return parse(expression, { ... });
} catch (e) {
throw new Error(`Failed to parse expression [${expression}].\n\nParse error: ${e.message}...`);
}
}
The throw propagates to callers in all three compiler targets (element, hydrate, server), which don't catch it. The Vite plugin's transform hook catches the error and reports it as a plugin error, killing the page.
Affected expression types
Every expression type passes through doParse:
parseCondition—if="..."attributesparseTextExpression—{text}bindingsparseClassExpression—class="..."with bindingsparseAttributeExpression— dynamic attributesparseBooleanAttributeExpression—disabled="condition"parseComponentPropExpression— component prop bindingsparseAccessor— property pathsparseStyleDeclarations— inline style bindingsparseServerCondition— server-side conditionsparseTemplateParts— template literal partsfindHtmlStringBindings— HTML string binding extraction
Problem
- Page crash — a single invalid expression kills the entire page
- No graceful degradation — the designer sees a Vite error overlay instead of the page with the broken part marked
- AIditor incompatibility — design tools may generate expressions the framework doesn't support yet. The page should render with the invalid parts flagged, not crash entirely.
Design
Convert parse errors to validation messages
Instead of throwing, doParse should catch parse errors and return a RenderFragment with validation messages. The compiler targets already handle RenderFragment.validations — they collect them and report them without crashing.
function doParse(expression: string, startRule: string, vars?: Variables) {
try {
return parse(expression, { ... });
} catch (e) {
const message = `Failed to parse expression [${expression}]: ${e.message}`;
const fallback = getFallbackForRule(startRule, expression);
return fallback;
}
}
The rendered output uses a type-appropriate fallback so the compiled code doesn't crash at runtime. The validation message is collected and reported through the normal validation pipeline.
Fallback values by expression type
String expressions render a visible error marker so the designer can spot the problem in the page. Boolean expressions return false silently.
| Expression type | Fallback rendered value | Effect |
|---|---|---|
conditionFunc |
vs => false |
Conditional element hidden |
condition |
false |
Condition evaluates to false |
dynamicText |
dt(vs => '[INVALID: expression]') |
Visible error in page |
dynamicAttribute |
da(vs => '[INVALID: expression]') |
Visible error in attribute |
classExpression |
'' |
No classes added |
booleanAttribute |
ba(vs => false) |
Attribute not present |
dynamicComponentProp |
vs => '[INVALID: expression]' |
Visible error in prop value |
accessor |
Null accessor with validation error | No data, error collected |
styleDeclarations |
Empty style object | No inline styles |
What the designer sees
In dev mode, the page renders. Broken string expressions show [INVALID: expression] visibly in the page so the designer can spot them. Broken boolean expressions silently hide the conditional element. The browser console shows the validation details. The Vite error overlay does NOT appear — the page is usable.
The jay-stack validate CLI reports all expression errors as validation messages.
No behavior change for valid expressions
This only changes error handling. Valid expressions parse and compile exactly as before.
parseAccessor graceful fallback
parseAccessor returns an Accessor with validation errors for unknown fields, but throws on parse errors. The same pattern applies — return a null accessor (renders as undefined, carries validation messages) instead of throwing.
Multiple root elements in body
The compiler's ensureSingleChildElement expects one root element in <body> and returns a validation error when there are multiple. This crashes the page.
Design tools (and manual editing) can easily produce multiple root elements:
<body>
<header>...</header>
<main>...</main>
<footer>...</footer>
</body>
Auto-wrap approach: when the body has multiple child elements, wrap them in a <div> with display: contents so it's layout-transparent:
<body>
<div style="display: contents">
<header>...</header>
<main>...</main>
<footer>...</footer>
</div>
</body>
display: contents makes the wrapper invisible to CSS layout — the children behave as if they're direct children of <body>. The wrapper exists only to satisfy the single-root constraint for coordinate assignment and hydration.
This is preferred over a validation error because:
- Multiple root elements are a common, natural HTML pattern
- The designer shouldn't need to know about the single-root constraint
- The
display: contentswrapper has no visual or layout side effects
Implementation Plan
Phase 1: Make doParse return validation errors
compiler-jay-html/lib/expressions/expression-compiler.ts:
- Change
doParseto catch errors and returnRenderFragmentwith validations - Add a
fallbackparameter or derive it fromstartRuleto produce the right safe value - For
parseAccessor, return anAccessorwith validations instead of throwing
Phase 2: Auto-wrap multiple root elements
compiler-jay-html/lib/jay-target/jay-html-compiler.ts, jay-html-compiler-hydrate.ts, jay-html-compiler-server.ts:
- In
ensureSingleChildElement(or its callers), when multiple children are found, wrap them in<div style="display: contents">instead of returning a validation error
Phase 3: Verify compiler targets handle validations
jay-html-compiler.ts, jay-html-compiler-hydrate.ts, jay-html-compiler-server.ts:
- Verify that
RenderFragment.validationsfrom parse functions are propagated to the page-level validation list - Most call sites already merge
.validations— check for any that assume no validations
Phase 4: Actionable validation messages
Validation messages must give an AI agent (or human) enough information to fix the problem. Each message should include:
- What failed — the expression and which attribute it was in
- Why it failed — the parse error (unexpected token, unknown operator)
- How to fix — pointer to the relevant agent-kit guide
Example:
[page.jay-html:42] Failed to parse condition: currentPath ^= '/docs'
Parse error: unexpected operator "^=" — did you mean "===" or "!=="?
See: agent-kit/designer/jay-html-template-syntax.md → Conditional Rendering
For expressions using unsupported syntax:
[page.jay-html:15] Failed to parse expression: {item.price | currency}
Parse error: pipe operators are not supported in jay-html expressions.
Supported syntax: {field}, {field.nested}, {condition ? classA : classB}
See: agent-kit/designer/jay-html-template-syntax.md → Data Binding
Phase 5: Tests
- Add test cases for malformed expressions: missing field, unknown operator, unclosed parenthesis
- Verify they produce validation messages instead of throwing
- Verify the fallback values don't cause runtime crashes
- Add test for multiple root elements producing a
display: contentswrapper
Phase 6: Slow render
expression-parser.pegjs slow render rules:
parseConditionForSlowRenderalso callsdoParse— verify it gets the same graceful handling
Phase 7: Documentation
Agent-kit designer/jay-html-template-syntax.md:
- Add a "Common Errors" section listing typical parse errors and their fixes
- Document the supported expression syntax clearly so agents know what's valid
Top-level docs/core/jay-html.md:
- Document the graceful error handling behavior (validation messages, visible markers)
Compiler compiler-jay-html/docs/jay-html-docs.md:
- Document the fallback value strategy per expression type
Phase 8: Validate
- Run
yarn confirmfrom monorepo root — all tests, type checks, and formatting must pass
Trade-offs
| Choice | Pro | Con |
|---|---|---|
| Validation errors (proposed) | Page renders, designer can work, errors collected | Bad expression silently produces wrong output |
| Throw (current) | Fails fast, error is obvious | Page crashes, blocks designer workflow |
| Render visible error marker | Designer spots issues easily | Clutters the design, may confuse non-technical users |
| Auto-wrap multiple roots | Transparent, matches HTML convention | Adds a DOM node (but display: contents hides it) |
| Reject multiple roots (current) | Enforces clean structure | Crashes on common HTML pattern, blocks designer |
Implementation Results
What was implemented
All phases from the plan, no deviations from the design.
expression-compiler.ts:
getFallbackForRulefunction returns type-appropriate fallback perstartRule(string expressions →[INVALID: expr], booleans →false, etc.)doParsecatches parse errors and returns fallback with validation messages + console warningthrowOnErrorparameter for structural parsing (parseImportNames,parseEnumValues) that must still throwparseAccessorreturns nullAccessorwithJayUnknowntype on parse errorgetExpressionHelpupdated with^=and string comparison examples- All validation messages include
See: agent-kit/designer/jay-html-template-syntax.mdpointer
jay-html-helpers.ts:
ensureSingleChildElementauto-wraps multiple body children in<div style="display: contents">- Zero elements still returns validation error
jay-html-compiler-server.ts:
- Fixed validation propagation for
parseServerConditionat conditional and headless instance sites
Documentation:
- Agent-kit
jay-html-template-syntax.md: "Common Errors" table docs/core/jay-html.md: Error Handling sectioncompiler-jay-html/docs/jay-html-docs.md: Fallback strategy table and multi-root wrapping
Tests
6 new tests in expression-compiler.unit.test.ts:
- Malformed condition →
vs => falsewith validation - Malformed text expression →
[INVALID: ...]with validation - Malformed class expression → empty string with validation
- Malformed accessor →
JayUnknownaccessor with validation - Malformed boolean attribute →
ba(vs => false)with validation - Validation message includes guide reference
2 existing tests updated from toThrow to check validations (text expression, react text expression).
All 692 compiler tests pass. yarn confirm passes clean.
Log Methodology Note
Note: These design logs are written primarily for AI agents as part of the Design Log methodology and made accessible here for human readers. The language and structure are optimized for machine consumption — expect precise, specification-style prose rather than narrative documentation.